Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cms/djangoapps/contentstore/tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from xmodule.tests.test_export import PureXBlock

from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
from cms.djangoapps.contentstore.views.preview import _preview_module_system
from cms.djangoapps.contentstore.views.preview import _prepare_runtime_for_preview
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.lib.edx_six import get_gettext

Expand Down Expand Up @@ -70,7 +70,7 @@ def setUp(self):
self.course = CourseFactory.create()
self.field_data = mock.Mock()
self.descriptor = BlockFactory(category="pure", parent=self.course)
self.runtime = _preview_module_system(
_prepare_runtime_for_preview(
self.request,
self.descriptor,
self.field_data,
Expand All @@ -81,7 +81,7 @@ def get_block_i18n_service(self, descriptor):
"""
return the block i18n service.
"""
i18n_service = self.runtime.service(descriptor, 'i18n')
i18n_service = self.descriptor.runtime.service(descriptor, 'i18n')
self.assertIsNotNone(i18n_service)
self.assertIsInstance(i18n_service, XBlockI18nService)
return i18n_service
Expand Down Expand Up @@ -171,7 +171,7 @@ def test_i18n_service_callable(self):
"""
Test: i18n service should be callable in studio.
"""
self.assertTrue(callable(self.runtime._services.get('i18n'))) # pylint: disable=protected-access
self.assertTrue(callable(self.descriptor.runtime._services.get('i18n'))) # pylint: disable=protected-access


class InternationalizationTest(ModuleStoreTestCase):
Expand Down
49 changes: 17 additions & 32 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,38 +293,23 @@ def can_write(self, course_key):
return has_studio_write_access(self._user, course_key)


class StudioEditModuleRuntime:
def load_services_for_studio(runtime, user):
"""
An extremely minimal ModuleSystem shim used for XBlock edits and studio_view.
(i.e. whenever we're not using PreviewModuleSystem.) This is required to make information
Function to set some required services used for XBlock edits and studio_view.
(i.e. whenever we're not loading _prepare_runtime_for_preview.) This is required to make information
about the current user (especially permissions) available via services as needed.
"""
services = {
"user": DjangoXBlockUserService(user),
"studio_user_permissions": StudioPermissionsService(user),
"mako": MakoService(),
"settings": SettingsService(),
"lti-configuration": ConfigurationService(CourseAllowPIISharingInLTIFlag),
"teams_configuration": TeamsConfigurationService(),
"library_tools": LibraryToolsService(modulestore(), user.id)
}

def __init__(self, user):
self._user = user

def service(self, block, service_name):
"""
This block is not bound to a user but some blocks (LibraryContentBlock) may need
user-specific services to check for permissions, etc.
If we return None here, CombinedSystem will load services from the descriptor runtime.
"""
if block.service_declaration(service_name) is not None:
if service_name == "user":
return DjangoXBlockUserService(self._user)
if service_name == "studio_user_permissions":
return StudioPermissionsService(self._user)
if service_name == "mako":
return MakoService()
if service_name == "settings":
return SettingsService()
if service_name == "lti-configuration":
return ConfigurationService(CourseAllowPIISharingInLTIFlag)
if service_name == "teams_configuration":
return TeamsConfigurationService()
if service_name == "library_tools":
return LibraryToolsService(modulestore(), self._user.id)
return None
runtime._services.update(services) # lint-amnesty, pylint: disable=protected-access


@require_http_methods("GET")
Expand Down Expand Up @@ -368,8 +353,8 @@ def xblock_view_handler(request, usage_key_string, view_name):
))

if view_name in (STUDIO_VIEW, VISIBILITY_VIEW):
if view_name == STUDIO_VIEW and xblock.xmodule_runtime is None:
xblock.xmodule_runtime = StudioEditModuleRuntime(request.user)
if view_name == STUDIO_VIEW:
load_services_for_studio(xblock.runtime, request.user)

try:
fragment = xblock.render(view_name)
Expand Down Expand Up @@ -524,7 +509,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
old_metadata = own_metadata(xblock)
if old_content is None:
old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)
xblock.xmodule_runtime = StudioEditModuleRuntime(user)
load_services_for_studio(xblock.runtime, user)
xblock.editor_saved(user, old_metadata, old_content)

# Update after the callback so any changes made in the callback will get persisted.
Expand Down Expand Up @@ -937,7 +922,7 @@ def _duplicate_block(parent_usage_key, duplicate_source_usage_key, user, display
# Allow an XBlock to do anything fancy it may need to when duplicated from another block.
# These blocks may handle their own children or parenting if needed. Let them return booleans to
# let us know if we need to handle these or not.
dest_block.xmodule_runtime = StudioEditModuleRuntime(user)
load_services_for_studio(dest_block.runtime, user)
children_handled = dest_block.studio_post_duplicate(store, source_item)

# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
Expand Down
4 changes: 2 additions & 2 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

from ..utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url
from .helpers import get_parent_xblock, is_unit, xblock_type_display_name
from .block import StudioEditModuleRuntime, add_container_page_publishing_info, create_xblock_info
from .block import add_container_page_publishing_info, create_xblock_info, load_services_for_studio

__all__ = [
'container_handler',
Expand Down Expand Up @@ -560,7 +560,7 @@ def component_handler(request, usage_key_string, handler, suffix=''):
descriptor = modulestore().get_item(usage_key)
handler_descriptor = descriptor
asides = []
handler_descriptor.xmodule_runtime = StudioEditModuleRuntime(request.user)
load_services_for_studio(handler_descriptor.runtime, request.user)
resp = handler_descriptor.handle(handler, req, suffix)
except NoSuchHandlerError:
log.info("XBlock %s attempted to access missing handler %r", handler_descriptor, handler, exc_info=True)
Expand Down
178 changes: 93 additions & 85 deletions cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from xmodule.studio_editable import has_author_view
from xmodule.util.sandboxing import SandboxService
from xmodule.util.xmodule_django import add_webpack_to_fragment
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, ModuleSystem
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW
from cms.djangoapps.xblock_config.models import StudioConfig
from cms.djangoapps.contentstore.toggles import individualize_anonymous_user_id, ENABLE_COPY_PASTE_FEATURE
from cms.lib.xblock.field_data import CmsFieldData
Expand All @@ -41,8 +41,7 @@
request_token,
wrap_fragment,
wrap_xblock,
wrap_xblock_aside,
xblock_local_resource_url
wrap_xblock_aside
)

from ..utils import get_visibility_partition_info
Expand Down Expand Up @@ -94,74 +93,72 @@ def preview_handler(request, usage_key_string, handler, suffix=''):
return webob_to_django_response(resp)


class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def handler_url(block, handler_name, suffix='', query='', thirdparty=False): # lint-amnesty, pylint: disable=unused-argument
"""
An XModule ModuleSystem for use in Studio previews
Handler URL function for Preview
"""
# xblocks can check for this attribute during rendering to determine if
# they are being rendered for preview (i.e. in Studio)
is_author_mode = True
return reverse('preview_handler', kwargs={
'usage_key_string': str(block.scope_ids.usage_id),
'handler': handler_name,
'suffix': suffix,
}) + '?' + query

def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
return reverse('preview_handler', kwargs={
'usage_key_string': str(block.scope_ids.usage_id),
'handler': handler_name,
'suffix': suffix,
}) + '?' + query

def local_resource_url(self, block, uri):
return xblock_local_resource_url(block, uri)
def preview_applicable_aside_types(block, applicable_aside_types=None):
"""
Remove acid_aside and honor the config record
"""
if not StudioConfig.asides_enabled(block.scope_ids.block_type):
return []

# TODO: aside_type != 'acid_aside' check should be removed once AcidBlock is only installed during tests
# (see https://openedx.atlassian.net/browse/TE-811)
return [
aside_type
for aside_type in applicable_aside_types(block)
if aside_type != 'acid_aside'
]


def render_child_placeholder(block, view_name, context, wrap_block=None):
"""
Renders a placeholder XBlock.
"""
return wrap_block(block, view_name, Fragment(), context)

def applicable_aside_types(self, block):
"""
Remove acid_aside and honor the config record
"""
if not StudioConfig.asides_enabled(block.scope_ids.block_type):
return []

# TODO: aside_type != 'acid_aside' check should be removed once AcidBlock is only installed during tests
# (see https://openedx.atlassian.net/browse/TE-811)
return [
aside_type
for aside_type in super().applicable_aside_types(block)
if aside_type != 'acid_aside'
]

def render_child_placeholder(self, block, view_name, context):
"""
Renders a placeholder XBlock.
"""
return self.wrap_xblock(block, view_name, Fragment(), context)

def layout_asides(self, block, context, frag, view_name, aside_frag_fns):
position_for_asides = '<!-- footer for xblock_aside -->'
result = Fragment()
result.add_fragment_resources(frag)
def preview_layout_asides(block, context, frag, view_name, aside_frag_fns, wrap_aside=None):
"""
Custom layout of asides for preview
"""
position_for_asides = '<!-- footer for xblock_aside -->'
result = Fragment()
result.add_fragment_resources(frag)

for aside, aside_fn in aside_frag_fns:
aside_frag = aside_fn(block, context)
if aside_frag.content != '':
aside_frag_wrapped = self.wrap_aside(block, aside, view_name, aside_frag, context)
aside.save()
result.add_fragment_resources(aside_frag_wrapped)
replacement = position_for_asides + aside_frag_wrapped.content
frag.content = frag.content.replace(position_for_asides, replacement)
for aside, aside_fn in aside_frag_fns:
aside_frag = aside_fn(block, context)
if aside_frag.content != '':
aside_frag_wrapped = wrap_aside(block, aside, view_name, aside_frag, context)
aside.save()
result.add_fragment_resources(aside_frag_wrapped)
replacement = position_for_asides + aside_frag_wrapped.content
frag.content = frag.content.replace(position_for_asides, replacement)

result.add_content(frag.content)
return result
result.add_content(frag.content)
return result


def _preview_module_system(request, descriptor, field_data):
def _prepare_runtime_for_preview(request, block, field_data):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering block previews.
Sets properties in the runtime of the specified block that is
required for rendering block previews.

request: The active django request
descriptor: An XModuleDescriptor
field_data: Wrapped field data for previews
"""

course_id = descriptor.location.course_key
display_name_only = (descriptor.category == 'static_tab')
course_id = block.location.course_key
display_name_only = (block.category == 'static_tab')

replace_url_service = ReplaceURLService(course_id=course_id)

Expand Down Expand Up @@ -201,36 +198,48 @@ def _preview_module_system(request, descriptor, field_data):
# the anonymous_user_id to specific courses. These are captured in the
# block attribute 'requires_per_student_anonymous_id'. Please note,
# the course_id field in AnynomousUserID model is blank if value is None.
if getattr(descriptor, 'requires_per_student_anonymous_id', False):
if getattr(block, 'requires_per_student_anonymous_id', False):
preview_anonymous_user_id = anonymous_id_for_user(request.user, None)
else:
preview_anonymous_user_id = anonymous_id_for_user(request.user, course_id)

return PreviewModuleSystem(
get_block=partial(_load_preview_block, request),
mixins=settings.XBLOCK_MIXINS,

# Set up functions to modify the fragment produced by student_view
wrappers=wrappers,
wrappers_asides=wrappers_asides,
# Get the raw DescriptorSystem, not the CombinedSystem
descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access
services={
"field-data": field_data,
"i18n": XBlockI18nService,
'mako': mako_service,
"settings": SettingsService(),
"user": DjangoXBlockUserService(
request.user,
user_role=get_user_role(request.user, course_id),
anonymous_user_id=preview_anonymous_user_id,
),
"partitions": StudioPartitionService(course_id=course_id),
"teams_configuration": TeamsConfigurationService(),
"sandbox": SandboxService(contentstore=contentstore, course_id=course_id),
"cache": CacheService(cache),
'replace_urls': replace_url_service
},
services = {
"field-data": field_data,
"i18n": XBlockI18nService,
'mako': mako_service,
"settings": SettingsService(),
"user": DjangoXBlockUserService(
request.user,
user_role=get_user_role(request.user, course_id),
anonymous_user_id=preview_anonymous_user_id,
),
"partitions": StudioPartitionService(course_id=course_id),
"teams_configuration": TeamsConfigurationService(),
"sandbox": SandboxService(contentstore=contentstore, course_id=course_id),
"cache": CacheService(cache),
'replace_urls': replace_url_service
}

block.runtime.get_block_for_descriptor = partial(_load_preview_block, request)
block.runtime.mixins = settings.XBLOCK_MIXINS

# Set up functions to modify the fragment produced by student_view
block.runtime.wrappers = wrappers
block.runtime.wrappers_asides = wrappers_asides
block.runtime._runtime_services.update(services) # lint-amnesty, pylint: disable=protected-access

# xmodules can check for this attribute during rendering to determine if
# they are being rendered for preview (i.e. in Studio)
block.runtime.is_author_mode = True
block.runtime.handler_url_override = handler_url
block.runtime.applicable_aside_types_override = preview_applicable_aside_types
block.runtime.render_child_placeholder = partial(
render_child_placeholder,
wrap_block=block.runtime.wrap_xblock
)
block.runtime.layout_asides_override = partial(
preview_layout_asides,
wrap_aside=block.runtime.wrap_aside
)


Expand Down Expand Up @@ -261,12 +270,11 @@ def _load_preview_block(request, descriptor):
else:
wrapper = partial(LmsFieldData, student_data=student_data)

# wrap the _field_data upfront to pass to _preview_module_system
# wrap the _field_data upfront to pass to _prepare_runtime_for_preview
wrapped_field_data = wrapper(descriptor._field_data) # pylint: disable=protected-access
preview_runtime = _preview_module_system(request, descriptor, wrapped_field_data)
_prepare_runtime_for_preview(request, descriptor, wrapped_field_data)

descriptor.bind_for_student(
preview_runtime,
request.user.id,
[wrapper]
)
Expand Down
3 changes: 1 addition & 2 deletions cms/djangoapps/contentstore/views/tests/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -2128,8 +2128,7 @@ def test_add_groups(self):
group_id_to_child = split_test.group_id_to_child.copy()
self.assertEqual(2, len(group_id_to_child))

# Test environment and Studio use different module systems
# (CachingDescriptorSystem is used in tests, PreviewModuleSystem in Studio).
# CachingDescriptorSystem is used in tests.
# CachingDescriptorSystem doesn't have user service, that's needed for
# SplitTestBlock. So, in this line of code we add this service manually.
split_test.runtime._services['user'] = DjangoXBlockUserService(self.user) # pylint: disable=protected-access
Expand Down
Loading