diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index ff3fb46616..d9981e5740 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -9,6 +9,8 @@ from xblock.fields import Scope from xblock_django.models import XBlockStudioConfigurationFlag from xmodule.modulestore.django import modulestore +from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG + class CourseMetadata(object): ''' @@ -63,7 +65,7 @@ class CourseMetadata(object): ] @classmethod - def filtered_list(cls): + def filtered_list(cls, course_key=None): """ Filter fields based on feature flag, i.e. enabled, disabled. """ @@ -117,6 +119,10 @@ class CourseMetadata(object): if not XBlockStudioConfigurationFlag.is_enabled(): filtered_list.append('allow_unsupported_xblocks') + # Do not show "Course Visibility For Unenrolled Learners" in Studio Advanced Settings + # if the enable_anonymous_access flag is not enabled + if not COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key=course_key): + filtered_list.append('course_visibility') return filtered_list @classmethod @@ -128,7 +134,7 @@ class CourseMetadata(object): result = {} metadata = cls.fetch_all(descriptor) for key, value in metadata.iteritems(): - if key in cls.filtered_list(): + if key in cls.filtered_list(descriptor.id): continue result[key] = value return result @@ -163,7 +169,7 @@ class CourseMetadata(object): Ensures none of the fields are in the blacklist. """ - filtered_list = cls.filtered_list() + filtered_list = cls.filtered_list(descriptor.id) # Don't filter on the tab attribute if filter_tabs is False. if not filter_tabs: filtered_list.remove("tabs") @@ -199,7 +205,7 @@ class CourseMetadata(object): errors: list of error objects result: the updated course metadata or None if error """ - filtered_list = cls.filtered_list() + filtered_list = cls.filtered_list(descriptor.id) if not filter_tabs: filtered_list.remove("tabs") diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 574058e5f1..1471489977 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -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: { diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 6eaafbe38c..de66aee0c1 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -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. diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 2cd8d29114..dfb79e2ac7 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -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__) @@ -247,7 +247,13 @@ 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, banner_text) + return self._student_or_public_view(context, banner_text) + + def public_view(self, context): + """ + Renders the preview view of the block in the LMS. + """ + return self._student_or_public_view(context or {}, None, PUBLIC_VIEW) def _special_exam_student_view(self): """ @@ -299,7 +305,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, banner_text=None): + def _student_or_public_view(self, context, 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 @@ -320,8 +326,9 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): 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, @@ -330,8 +337,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)) @@ -423,7 +430,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 @@ -461,7 +468,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 = { diff --git a/common/lib/xmodule/xmodule/tests/helpers.py b/common/lib/xmodule/xmodule/tests/helpers.py index 9cb4decab1..732dff94f8 100644 --- a/common/lib/xmodule/xmodule/tests/helpers.py +++ b/common/lib/xmodule/xmodule/tests/helpers.py @@ -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 diff --git a/common/lib/xmodule/xmodule/tests/test_html_module.py b/common/lib/xmodule/xmodule/tests/test_html_module.py index 819cc01f82..6d29a11071 100644 --- a/common/lib/xmodule/xmodule/tests/test_html_module.py +++ b/common/lib/xmodule/xmodule/tests/test_html_module.py @@ -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 = '
This is a test
' + 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() diff --git a/common/lib/xmodule/xmodule/tests/test_sequence.py b/common/lib/xmodule/xmodule/tests/test_sequence.py index ea739e731d..dc28208ff8 100644 --- a/common/lib/xmodule/xmodule/tests/test_sequence.py +++ b/common/lib/xmodule/xmodule/tests/test_sequence.py @@ -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'), ) diff --git a/common/lib/xmodule/xmodule/tests/test_vertical.py b/common/lib/xmodule/xmodule/tests/test_vertical.py index c272b95be3..69d5660fe6 100644 --- a/common/lib/xmodule/xmodule/tests/test_vertical.py +++ b/common/lib/xmodule/xmodule/tests/test_vertical.py @@ -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( diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py index 1cc44d69de..924edc3267 100644 --- a/common/lib/xmodule/xmodule/vertical_block.py +++ b/common/lib/xmodule/xmodule/vertical_block.py @@ -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 import webpack_loader.utils @@ -45,9 +45,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 = [] @@ -57,12 +57,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() @@ -82,7 +84,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({ @@ -90,20 +92,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. diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 3c3b39ce50..52a208edd3 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -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 @@ -376,6 +383,7 @@ class XModuleMixin(XModuleFields, XBlock): migrate and test switching to display_name_with_default, which is no longer escaped. """ + # xss-lint: disable=python-deprecated-display-name return block_metadata_utils.display_name_with_default_escaped(self) @property @@ -481,6 +489,7 @@ class XModuleMixin(XModuleFields, XBlock): if self.has_children: return sum((child.get_content_titles() for child in self.get_children()), []) else: + # xss-lint: disable=python-deprecated-display-name return [self.display_name_with_default_escaped] def get_children(self, usage_id_filter=None, usage_key_filter=None): # pylint: disable=arguments-differ @@ -757,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'' + ) + return Fragment(alert_html.format(DEFAULT_PUBLIC_VIEW_MESSAGE)) + class ProxyAttribute(object): """ @@ -852,6 +872,7 @@ class XModule(HTMLSnippet, XModuleMixin): self._runtime = value def __unicode__(self): + # xss-lint: disable=python-wrap-html return u'