Adds CouseModule.course_visibility and XBlock.public_view() for unenrolled users access to courses.

The course_visiblity field can have one of three values:
1. private (default): This keeps the standard access rules.
2. public_outline: Allows unenrolled and anonymous users access to the outline.
3. public: Allows unenrolled and anonymous users access to both outline and
   course content.

When an unenrolled user accesses course content, instead of student_view(),
public_view() is used. A default implementation is provided for XBlocks
which do not implement this view. The public_view() must not have any
functionality which assumes the presence of a valid User and should show
a readonly only interface for the XBlock content.
This commit is contained in:
Paulo Viadanna
2018-07-05 17:45:17 -03:00
committed by Usman Khalid
parent 2d338b34aa
commit c4fc4b5df6
21 changed files with 438 additions and 148 deletions

View File

@@ -44,6 +44,10 @@ DEFAULT_COURSE_VISIBILITY_IN_CATALOG = getattr(
DEFAULT_MOBILE_AVAILABLE = getattr(settings, 'DEFAULT_MOBILE_AVAILABLE', False)
COURSE_VISIBILITY_PRIVATE = 'private'
COURSE_VISIBILITY_PUBLIC_OUTLINE = 'public_outline'
COURSE_VISIBILITY_PUBLIC = 'public'
class StringOrDate(Date):
def from_json(self, value):
@@ -814,6 +818,21 @@ class CourseFields(object):
scope=Scope.settings
)
course_visibility = String(
display_name=_("Course Visibility For Unenrolled Learners"),
help=_(
"Defines the access permissions for unenrolled learners. This can be set to one of three values: "
"'private' (default visibility, only allowed for enrolled students), 'public_outline' "
"(allow access to course outline) and 'public' (allow access to both outline and course content)."
),
default=COURSE_VISIBILITY_PRIVATE,
scope=Scope.settings,
values=[
{"display_name": _("private"), "value": COURSE_VISIBILITY_PRIVATE},
{"display_name": _("public_outline"), "value": COURSE_VISIBILITY_PUBLIC_OUTLINE},
{"display_name": _("public"), "value": COURSE_VISIBILITY_PUBLIC}]
)
"""
instructor_info dict structure:
{

View File

@@ -79,6 +79,13 @@ class HtmlBlock(object):
"""
return Fragment(self.get_html())
@XBlock.supports("multi_device")
def public_view(self, context):
"""
Returns a fragment that contains the html for the preview view
"""
return self.student_view(context)
def student_view_data(self, context=None): # pylint: disable=unused-argument
"""
Return a JSON representation of the student_view of this XBlock.

View File

@@ -18,7 +18,7 @@ from mock import patch
from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase, FilteredQueryCountMixin
from openedx.core.lib.tempdir import mkdtemp_clean
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from student.tests.factories import AdminFactory, UserFactory
from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import SignalHandler, clear_existing_modulestores, modulestore
@@ -343,6 +343,8 @@ class ModuleStoreTestUsersMixin():
# Set up the test user
if is_unenrolled_staff:
user = StaffFactory(course_key=course.id, password=self.TEST_PASSWORD)
elif user_type is CourseUserType.GLOBAL_STAFF:
user = AdminFactory(password=self.TEST_PASSWORD)
else:
user = UserFactory(password=self.TEST_PASSWORD)
self.client.login(username=user.username, password=self.TEST_PASSWORD)

View File

@@ -22,7 +22,7 @@ from .exceptions import NotFoundError
from .fields import Date
from .mako_module import MakoModuleDescriptor
from .progress import Progress
from .x_module import STUDENT_VIEW, XModule
from .x_module import STUDENT_VIEW, PUBLIC_VIEW, XModule
from .xml_module import XmlDescriptor
log = logging.getLogger(__name__)
@@ -257,7 +257,18 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
banner_text, special_html = special_html_view
if special_html and not masquerading_as_specific_student:
return Fragment(special_html)
return self._student_view(context, prereq_met, prereq_meta_info, banner_text)
return self._student_or_public_view(context, prereq_met, prereq_meta_info, banner_text)
def public_view(self, context):
"""
Renders the preview view of the block in the LMS.
"""
prereq_met = True
prereq_meta_info = {}
if self._required_prereq():
prereq_met, prereq_meta_info = self._compute_is_prereq_met(True)
return self._student_or_public_view(context or {}, prereq_met, prereq_meta_info, None, PUBLIC_VIEW)
def _special_exam_student_view(self):
"""
@@ -309,7 +320,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
# NOTE (CCB): We default to true to maintain the behavior in place prior to allowing anonymous access access.
return context.get('user_authenticated', True)
def _student_view(self, context, prereq_met, prereq_meta_info, banner_text=None):
def _student_or_public_view(self, context, prereq_met, prereq_meta_info, banner_text=None, view=STUDENT_VIEW):
"""
Returns the rendered student view of the content of this
sequential. If banner_text is given, it is added to the
@@ -319,11 +330,14 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
self._update_position(context, len(display_items))
if prereq_met and not self._is_gate_fulfilled():
banner_text = _('This section is a prerequisite. You must complete this section in order to unlock additional content.')
banner_text = _(
'This section is a prerequisite. You must complete this section in order to unlock additional content.'
)
fragment = Fragment()
items = self._render_student_view_for_items(context, display_items, fragment, view) if prereq_met else []
params = {
'items': self._render_student_view_for_items(context, display_items, fragment) if prereq_met else [],
'items': items,
'element_id': self.location.html_id(),
'item_id': text_type(self.location),
'position': self.position,
@@ -332,8 +346,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'next_url': context.get('next_url'),
'prev_url': context.get('prev_url'),
'banner_text': banner_text,
'save_position': self.is_user_authenticated(context),
'show_completion': self.is_user_authenticated(context),
'save_position': view != PUBLIC_VIEW,
'show_completion': view != PUBLIC_VIEW,
'gated_content': self._get_gated_content_info(prereq_met, prereq_meta_info)
}
fragment.add_content(self.system.render_template("seq_module.html", params))
@@ -425,7 +439,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
elif self.position is None or self.position > number_of_display_items:
self.position = 1
def _render_student_view_for_items(self, context, display_items, fragment):
def _render_student_view_for_items(self, context, display_items, fragment, view=STUDENT_VIEW):
"""
Updates the given fragment with rendered student views of the given
display_items. Returns a list of dict objects with information about
@@ -463,7 +477,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
context['show_bookmark_button'] = show_bookmark_button
context['bookmarked'] = is_bookmarked
rendered_item = item.render(STUDENT_VIEW, context)
rendered_item = item.render(view, context)
fragment.add_fragment_resources(rendered_item)
iteminfo = {

View File

@@ -32,10 +32,19 @@ class StubUserService(UserService):
"""
Stub UserService for testing the sequence module.
"""
def __init__(self, is_anonymous=False, **kwargs):
self.is_anonymous = is_anonymous
super(StubUserService, self).__init__(**kwargs)
def get_current_user(self):
"""
Implements abstract method for getting the current user.
"""
user = XBlockUser()
user.opt_attrs['edx-platform.username'] = 'bilbo'
if self.is_anonymous:
user.opt_attrs['edx-platform.username'] = 'anonymous'
user.opt_attrs['edx-platform.is_authenticated'] = False
else:
user.opt_attrs['edx-platform.username'] = 'bilbo'
return user

View File

@@ -11,6 +11,7 @@ from xblock.fields import ScopeIds
from xmodule.html_module import CourseInfoModule, HtmlDescriptor, HtmlModule
from . import get_test_descriptor_system, get_test_system
from ..x_module import PUBLIC_VIEW, STUDENT_VIEW
def instantiate_descriptor(**field_data):
@@ -81,6 +82,22 @@ class HtmlModuleCourseApiTestCase(unittest.TestCase):
module = HtmlModule(descriptor, module_system, field_data, Mock())
self.assertEqual(module.student_view_data(), dict(enabled=True, html=html))
@ddt.data(
STUDENT_VIEW,
PUBLIC_VIEW,
)
def test_student_preview_view(self, view):
"""
Ensure that student_view and public_view renders correctly.
"""
html = '<p>This is a test</p>'
descriptor = Mock()
field_data = DictFieldData({'data': html})
module_system = get_test_system()
module = HtmlModule(descriptor, module_system, field_data, Mock())
rendered = module_system.render(module, view, {}).content
self.assertIn(html, rendered)
class HtmlModuleSubstitutionTestCase(unittest.TestCase):
descriptor = Mock()

View File

@@ -5,6 +5,7 @@ Tests for sequence module.
import json
from datetime import timedelta
import ddt
from django.utils.timezone import now
from freezegun import freeze_time
from mock import Mock, patch
@@ -12,7 +13,7 @@ from xmodule.seq_module import SequenceModule
from xmodule.tests import get_test_system
from xmodule.tests.helpers import StubUserService
from xmodule.tests.xml import factories as xml, XModuleXmlImportTest
from xmodule.x_module import STUDENT_VIEW
from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW
TODAY = now()
DUE_DATE = TODAY + timedelta(days=7)
@@ -20,6 +21,7 @@ PAST_DUE_BEFORE_END_DATE = TODAY + timedelta(days=14)
COURSE_END_DATE = TODAY + timedelta(days=21)
@ddt.ddt
class SequenceBlockTestCase(XModuleXmlImportTest):
"""
Base class for tests of Sequence Module.
@@ -92,7 +94,12 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
module_system.descriptor_runtime = block._runtime # pylint: disable=protected-access
block.xmodule_runtime = module_system
def _get_rendered_student_view(self, sequence, requested_child=None, extra_context=None, self_paced=False):
def _get_rendered_view(self,
sequence,
requested_child=None,
extra_context=None,
self_paced=False,
view=STUDENT_VIEW):
"""
Returns the rendered student view for the given sequence and the
requested_child parameter.
@@ -106,7 +113,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
with patch.object(SequenceModule, '_get_course') as mock_course:
self.course.self_paced = self_paced
mock_course.return_value = self.course
return sequence.xmodule_runtime.render(sequence, STUDENT_VIEW, context).content
return sequence.xmodule_runtime.render(sequence, view, context).content
def _assert_view_at_position(self, rendered_html, expected_position):
"""
@@ -118,10 +125,16 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
seq_module = SequenceModule(runtime=Mock(position=2), descriptor=Mock(), scope_ids=Mock())
self.assertEquals(seq_module.position, 2) # matches position set in the runtime
def test_render_student_view(self):
html = self._get_rendered_student_view(
@ddt.unpack
@ddt.data(
{'view': STUDENT_VIEW},
{'view': PUBLIC_VIEW},
)
def test_render_student_view(self, view):
html = self._get_rendered_view(
self.sequence_3_1,
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
view=view
)
self._assert_view_at_position(html, expected_position=1)
self.assertIn(unicode(self.sequence_3_1.location), html)
@@ -130,28 +143,40 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
self.assertIn("'prev_url': 'PrevSequential'", html)
self.assertNotIn("fa fa-check-circle check-circle is-hidden", html)
def test_student_view_first_child(self):
html = self._get_rendered_student_view(self.sequence_3_1, requested_child='first')
@ddt.unpack
@ddt.data(
{'view': STUDENT_VIEW},
{'view': PUBLIC_VIEW},
)
def test_student_view_first_child(self, view):
html = self._get_rendered_view(
self.sequence_3_1, requested_child='first', view=view
)
self._assert_view_at_position(html, expected_position=1)
def test_student_view_last_child(self):
html = self._get_rendered_student_view(self.sequence_3_1, requested_child='last')
@ddt.unpack
@ddt.data(
{'view': STUDENT_VIEW},
{'view': PUBLIC_VIEW},
)
def test_student_view_last_child(self, view):
html = self._get_rendered_view(self.sequence_3_1, requested_child='last', view=view)
self._assert_view_at_position(html, expected_position=3)
def test_tooltip(self):
html = self._get_rendered_student_view(self.sequence_3_1, requested_child=None)
html = self._get_rendered_view(self.sequence_3_1, requested_child=None)
for child in self.sequence_3_1.children:
self.assertIn("'page_title': '{}'".format(child.block_id), html)
def test_hidden_content_before_due(self):
html = self._get_rendered_student_view(self.sequence_4_1)
html = self._get_rendered_view(self.sequence_4_1)
self.assertIn("seq_module.html", html)
self.assertIn("'banner_text': None", html)
def test_hidden_content_past_due(self):
with freeze_time(COURSE_END_DATE):
progress_url = 'http://test_progress_link'
html = self._get_rendered_student_view(
html = self._get_rendered_view(
self.sequence_4_1,
extra_context=dict(progress_url=progress_url),
)
@@ -160,7 +185,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
def test_masquerade_hidden_content_past_due(self):
with freeze_time(COURSE_END_DATE):
html = self._get_rendered_student_view(
html = self._get_rendered_view(
self.sequence_4_1,
extra_context=dict(specific_masquerade=True),
)
@@ -173,14 +198,14 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
def test_hidden_content_self_paced_past_due_before_end(self):
with freeze_time(PAST_DUE_BEFORE_END_DATE):
html = self._get_rendered_student_view(self.sequence_4_1, self_paced=True)
html = self._get_rendered_view(self.sequence_4_1, self_paced=True)
self.assertIn("seq_module.html", html)
self.assertIn("'banner_text': None", html)
def test_hidden_content_self_paced_past_end(self):
with freeze_time(COURSE_END_DATE + timedelta(days=7)):
progress_url = 'http://test_progress_link'
html = self._get_rendered_student_view(
html = self._get_rendered_view(
self.sequence_4_1,
extra_context=dict(progress_url=progress_url),
self_paced=True,
@@ -248,7 +273,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
self.sequence_1_2.xmodule_runtime._services['gating'] = gating_mock_1_2 # pylint: disable=protected-access
self.sequence_1_2.display_name = 'sequence_1_2'
html = self._get_rendered_student_view(
html = self._get_rendered_view(
self.sequence_1_2,
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
)
@@ -261,7 +286,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
gating_mock_1_2.return_value.required_prereq.return_value = True
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [True, {}]
html = self._get_rendered_student_view(
html = self._get_rendered_view(
self.sequence_1_2,
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
)
@@ -274,7 +299,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
gating_mock_1_2.return_value.required_prereq.return_value = True
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [True, {}]
html = self._get_rendered_student_view(
html = self._get_rendered_view(
self.sequence_1_2,
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
)

View File

@@ -11,14 +11,12 @@ import json
import ddt
from fs.memoryfs import MemoryFS
from mock import Mock, patch
import six
from . import get_test_system
from .helpers import StubUserService
from .xml import XModuleXmlImportTest
from .xml import factories as xml
from ..x_module import STUDENT_VIEW, AUTHOR_VIEW
from ..x_module import STUDENT_VIEW, AUTHOR_VIEW, PUBLIC_VIEW
COMPLETION_DELAY = 9876
@@ -111,34 +109,41 @@ class VerticalBlockTestCase(BaseVerticalBlockTest):
"""
shard = 1
def assert_bookmark_info_in(self, content):
def assert_bookmark_info(self, assertion, content):
"""
Assert content has all the bookmark info.
Assert content has/hasn't all the bookmark info.
"""
self.assertIn('bookmark_id', content)
self.assertIn('{},{}'.format(self.username, unicode(self.vertical.location)), content)
self.assertIn('bookmarked', content)
self.assertIn('show_bookmark_button', content)
assertion('bookmark_id', content)
assertion('{},{}'.format(self.username, unicode(self.vertical.location)), content)
assertion('bookmarked', content)
assertion('show_bookmark_button', content)
@ddt.unpack
@ddt.data(
{'context': None},
{'context': {}}
{'context': None, 'view': STUDENT_VIEW},
{'context': {}, 'view': STUDENT_VIEW},
{'context': {}, 'view': PUBLIC_VIEW},
)
def test_render_student_view(self, context):
def test_render_student_preview_view(self, context, view):
"""
Test the rendering of the student view.
Test the rendering of the student and public view.
"""
self.module_system._services['bookmarks'] = Mock()
self.module_system._services['user'] = StubUserService()
self.module_system._services['completion'] = StubCompletionService(enabled=True, completion_value=0.0)
if view == STUDENT_VIEW:
self.module_system._services['user'] = StubUserService()
self.module_system._services['completion'] = StubCompletionService(enabled=True, completion_value=0.0)
elif view == PUBLIC_VIEW:
self.module_system._services['user'] = StubUserService(is_anonymous=True)
html = self.module_system.render(
self.vertical, STUDENT_VIEW, self.default_context if context is None else context
self.vertical, view, self.default_context if context is None else context
).content
self.assertIn(self.test_html_1, html)
self.assertIn(self.test_html_2, html)
self.assert_bookmark_info_in(html)
if view == STUDENT_VIEW:
self.assert_bookmark_info(self.assertIn, html)
else:
self.assert_bookmark_info(self.assertNotIn, html)
@ddt.unpack
@ddt.data(

View File

@@ -17,7 +17,7 @@ from xmodule.progress import Progress
from xmodule.seq_module import SequenceFields
from xmodule.studio_editable import StudioEditableBlock
from xmodule.util.xmodule_django import add_webpack_to_fragment
from xmodule.x_module import STUDENT_VIEW, XModuleFields
from xmodule.x_module import STUDENT_VIEW, PUBLIC_VIEW, XModuleFields
from xmodule.xml_module import XmlParserMixin
log = logging.getLogger(__name__)
@@ -43,9 +43,9 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
show_in_read_only_mode = True
def student_view(self, context):
def _student_or_public_view(self, context, view):
"""
Renders the student view of the block in the LMS.
Renders the requested view type of the block in the LMS.
"""
fragment = Fragment()
contents = []
@@ -55,12 +55,14 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
else:
child_context = {}
if 'bookmarked' not in child_context:
bookmarks_service = self.runtime.service(self, 'bookmarks')
child_context['bookmarked'] = bookmarks_service.is_bookmarked(usage_key=self.location), # pylint: disable=no-member
if 'username' not in child_context:
user_service = self.runtime.service(self, 'user')
child_context['username'] = user_service.get_current_user().opt_attrs['edx-platform.username']
if view == STUDENT_VIEW:
if 'bookmarked' not in child_context:
bookmarks_service = self.runtime.service(self, 'bookmarks')
child_context['bookmarked'] = bookmarks_service.is_bookmarked(
usage_key=self.location), # pylint: disable=no-member
if 'username' not in child_context:
user_service = self.runtime.service(self, 'user')
child_context['username'] = user_service.get_current_user().opt_attrs['edx-platform.username']
child_blocks = self.get_display_items()
@@ -80,7 +82,7 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
child_block_context['wrap_xblock_data'] = {
'mark-completed-on-view-after-delay': complete_on_view_delay
}
rendered_child = child.render(STUDENT_VIEW, child_block_context)
rendered_child = child.render(view, child_block_context)
fragment.add_fragment_resources(rendered_child)
contents.append({
@@ -88,20 +90,39 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
'content': rendered_child.content
})
fragment.add_content(self.system.render_template('vert_module.html', {
fragment_context = {
'items': contents,
'xblock_context': context,
'unit_title': self.display_name_with_default if not is_child_of_vertical else None,
'show_bookmark_button': child_context.get('show_bookmark_button', not is_child_of_vertical),
'bookmarked': child_context['bookmarked'],
'bookmark_id': u"{},{}".format(child_context['username'], unicode(self.location)), # pylint: disable=no-member
}))
}
if view == STUDENT_VIEW:
fragment_context.update({
'show_bookmark_button': child_context.get('show_bookmark_button', not is_child_of_vertical),
'bookmarked': child_context['bookmarked'],
'bookmark_id': u"{},{}".format(
child_context['username'], unicode(self.location)), # pylint: disable=no-member
})
fragment.add_content(self.system.render_template('vert_module.html', fragment_context))
add_webpack_to_fragment(fragment, 'VerticalStudentView')
fragment.initialize_js('VerticalStudentView')
return fragment
def student_view(self, context):
"""
Renders the student view of the block in the LMS.
"""
return self._student_or_public_view(context, STUDENT_VIEW)
def public_view(self, context):
"""
Renders the anonymous view of the block in the LMS.
"""
return self._student_or_public_view(context, PUBLIC_VIEW)
def author_view(self, context):
"""
Renders the Studio preview view, which supports drag and drop.

View File

@@ -39,6 +39,8 @@ from opaque_keys.edx.asides import AsideUsageKeyV2, AsideDefinitionKeyV2
from xmodule.exceptions import UndefinedContext
import dogstats_wrapper as dog_stats_api
from openedx.core.djangolib.markup import HTML
log = logging.getLogger(__name__)
XMODULE_METRIC_NAME = 'edxapp.xmodule'
@@ -55,6 +57,10 @@ DEPRECATION_VSCOMPAT_EVENT = 'deprecation.vscompat'
# the XBlock also implements author_view.
STUDENT_VIEW = 'student_view'
# This is the view that will be rendered to display the XBlock in the LMS for unenrolled learners.
# Implementations of this view should assume that a user and user data are not available.
PUBLIC_VIEW = 'public_view'
# An optional view of the XBlock similar to student_view, but with possible inline
# editing capabilities. This view differs from studio_view in that it should be as similar to student_view
# as possible. When previewing XBlocks within Studio, Studio will prefer author_view to student_view.
@@ -65,8 +71,9 @@ AUTHOR_VIEW = 'author_view'
STUDIO_VIEW = 'studio_view'
# Views that present a "preview" view of an xblock (as opposed to an editing view).
PREVIEW_VIEWS = [STUDENT_VIEW, AUTHOR_VIEW]
PREVIEW_VIEWS = [STUDENT_VIEW, PUBLIC_VIEW, AUTHOR_VIEW]
DEFAULT_PUBLIC_VIEW_MESSAGE = u'Please enroll to view this content.'
# Make '_' a no-op so we can scrape strings. Using lambda instead of
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
@@ -759,6 +766,17 @@ class XModuleMixin(XModuleFields, XBlock):
return metadata_field_editor_info
def public_view(self, _context):
"""
Default message for blocks that don't implement public_view
"""
alert_html = HTML(
u'<div class="page-banner"><div class="alert alert-warning">'
u'<span class="icon icon-alert fa fa fa-warning" aria-hidden="true"></span>'
u'<div class="message-content">{}</div></div></div>'
)
return Fragment(alert_html.format(DEFAULT_PUBLIC_VIEW_MESSAGE))
class ProxyAttribute(object):
"""
@@ -1223,6 +1241,7 @@ class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XModuleMixin):
get_score = module_attr('get_score')
handle_ajax = module_attr('handle_ajax')
student_view = module_attr(STUDENT_VIEW)
public_view = module_attr(PUBLIC_VIEW)
get_child_descriptors = module_attr('get_child_descriptors')
xmodule_handler = module_attr('xmodule_handler')