From 6e416310b952e8a85271111b3c790257b8cc26f8 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 11 Apr 2017 17:43:26 -0400 Subject: [PATCH] Show messages about component visibility. TNL-6746 --- .../contentstore/tests/test_utils.py | 22 +++--- cms/djangoapps/contentstore/utils.py | 10 +-- cms/djangoapps/contentstore/views/item.py | 9 ++- cms/djangoapps/contentstore/views/preview.py | 6 ++ cms/static/js/models/xblock_info.js | 72 +++++++++--------- .../views/pages/container_subviews_spec.js | 8 +- .../spec/views/pages/course_outline_spec.js | 34 +++++++++ .../js/views/pages/container_subviews.js | 72 +++++++++++------- cms/static/sass/elements/_modules.scss | 4 +- cms/static/sass/elements/_xblocks.scss | 18 +++-- cms/templates/js/course-outline.underscore | 73 ++++++++++++------- cms/templates/js/publish-xblock.underscore | 36 ++++++--- cms/templates/studio_xblock_wrapper.html | 24 +++--- .../test/acceptance/pages/studio/container.py | 9 +++ .../tests/studio/test_studio_container.py | 14 ++++ 15 files changed, 272 insertions(+), 139 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index f9d58d5047..3c8bb283db 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -391,8 +391,8 @@ class GroupVisibilityTest(CourseTestCase): def verify_all_components_visible_to_all(): # pylint: disable=invalid-name """ Verifies when group_access has not been set on anything. """ for item in (self.sequential, self.vertical, self.html, self.problem): - self.assertFalse(utils.has_children_visible_to_specific_content_groups(item)) - self.assertFalse(utils.is_visible_to_specific_content_groups(item)) + self.assertFalse(utils.has_children_visible_to_specific_partition_groups(item)) + self.assertFalse(utils.is_visible_to_specific_partition_groups(item)) verify_all_components_visible_to_all() @@ -409,16 +409,16 @@ class GroupVisibilityTest(CourseTestCase): self.set_group_access(self.vertical, {1: []}) self.set_group_access(self.problem, {2: [3, 4]}) - # Note that "has_children_visible_to_specific_content_groups" only checks immediate children. - self.assertFalse(utils.has_children_visible_to_specific_content_groups(self.sequential)) - self.assertTrue(utils.has_children_visible_to_specific_content_groups(self.vertical)) - self.assertFalse(utils.has_children_visible_to_specific_content_groups(self.html)) - self.assertFalse(utils.has_children_visible_to_specific_content_groups(self.problem)) + # Note that "has_children_visible_to_specific_partition_groups" only checks immediate children. + self.assertFalse(utils.has_children_visible_to_specific_partition_groups(self.sequential)) + self.assertTrue(utils.has_children_visible_to_specific_partition_groups(self.vertical)) + self.assertFalse(utils.has_children_visible_to_specific_partition_groups(self.html)) + self.assertFalse(utils.has_children_visible_to_specific_partition_groups(self.problem)) - self.assertTrue(utils.is_visible_to_specific_content_groups(self.sequential)) - self.assertFalse(utils.is_visible_to_specific_content_groups(self.vertical)) - self.assertFalse(utils.is_visible_to_specific_content_groups(self.html)) - self.assertTrue(utils.is_visible_to_specific_content_groups(self.problem)) + self.assertTrue(utils.is_visible_to_specific_partition_groups(self.sequential)) + self.assertFalse(utils.is_visible_to_specific_partition_groups(self.vertical)) + self.assertFalse(utils.is_visible_to_specific_partition_groups(self.html)) + self.assertTrue(utils.is_visible_to_specific_partition_groups(self.problem)) class GetUserPartitionInfoTest(ModuleStoreTestCase): diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4a97d056d4..a414f5815c 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -163,24 +163,24 @@ def is_currently_visible_to_students(xblock): return True -def has_children_visible_to_specific_content_groups(xblock): +def has_children_visible_to_specific_partition_groups(xblock): """ - Returns True if this xblock has children that are limited to specific content groups. + Returns True if this xblock has children that are limited to specific user partition groups. Note that this method is not recursive (it does not check grandchildren). """ if not xblock.has_children: return False for child in xblock.get_children(): - if is_visible_to_specific_content_groups(child): + if is_visible_to_specific_partition_groups(child): return True return False -def is_visible_to_specific_content_groups(xblock): +def is_visible_to_specific_partition_groups(xblock): """ - Returns True if this xblock has visibility limited to specific content groups. + Returns True if this xblock has visibility limited to specific user partition groups. """ if not xblock.group_access: return False diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 0da2dd07a5..0abd4970cf 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -28,7 +28,7 @@ from xblock_django.user_service import DjangoXBlockUserService from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW from contentstore.utils import ( find_release_date_source, find_staff_lock_source, is_currently_visible_to_students, - ancestor_has_staff_lock, has_children_visible_to_specific_content_groups, + ancestor_has_staff_lock, has_children_visible_to_specific_partition_groups, get_user_partition_info, get_split_group_display_name, ) from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \ @@ -1005,6 +1005,7 @@ def _get_module_info(xblock, rewrite_static_links=True, include_ancestor_info=Fa ) if include_publishing_info: add_container_page_publishing_info(xblock, xblock_info) + return xblock_info @@ -1217,6 +1218,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ) else: xblock_info['staff_only_message'] = False + + xblock_info["has_partition_group_components"] = has_children_visible_to_specific_partition_groups( + xblock + ) return xblock_info @@ -1245,7 +1250,7 @@ def add_container_page_publishing_info(xblock, xblock_info): # pylint: disable= xblock_info["edited_by"] = safe_get_username(xblock.subtree_edited_by) xblock_info["published_by"] = safe_get_username(xblock.published_by) xblock_info["currently_visible_to_students"] = is_currently_visible_to_students(xblock) - xblock_info["has_content_group_components"] = has_children_visible_to_specific_content_groups(xblock) + xblock_info["has_partition_group_components"] = has_children_visible_to_specific_partition_groups(xblock) if xblock_info["release_date"]: xblock_info["release_date_from"] = _get_release_date_from(xblock) if xblock_info["visibility_state"] == VisibilityState.staff_only: diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 361b6817a5..17699427a9 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -7,6 +7,7 @@ from django.conf import settings from django.core.urlresolvers import reverse from django.http import Http404, HttpResponseBadRequest from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ from edxmako.shortcuts import render_to_string from openedx.core.lib.xblock_utils import ( @@ -38,6 +39,7 @@ import static_replace from .session_kv_store import SessionKeyValueStore from .helpers import render_from_lms +from contentstore.utils import get_visibility_partition_info from contentstore.views.access import get_user_role from xblock_config.models import StudioConfig @@ -279,6 +281,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): root_xblock = context.get('root_xblock') is_root = root_xblock and xblock.location == root_xblock.location is_reorderable = _is_xblock_reorderable(xblock, context) + selected_groups_label = get_visibility_partition_info(xblock)['selected_groups_label'] + if selected_groups_label: + selected_groups_label = _('Visible to: {list_of_groups}').format(list_of_groups=selected_groups_label) template_context = { 'xblock_context': context, 'xblock': xblock, @@ -288,6 +293,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): 'is_reorderable': is_reorderable, 'can_edit': context.get('can_edit', True), 'can_edit_visibility': context.get('can_edit_visibility', True), + 'selected_groups_label': selected_groups_label, 'can_add': context.get('can_add', True), 'can_move': context.get('can_move', True) } diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 7c44a07216..e55bd2c4d9 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -9,46 +9,46 @@ function(Backbone, _, str, ModuleUtils) { // NOTE: 'publish' is not an attribute on XBlockInfo, but it is used to signal the publish // and discard changes actions. Therefore 'publish' cannot be introduced as an attribute. defaults: { - 'id': null, - 'display_name': null, - 'category': null, - 'data': null, - 'metadata': null, + id: null, + display_name: null, + category: null, + data: null, + metadata: null, /** * The Studio URL for this xblock, or null if it doesn't have one. */ - 'studio_url': null, + studio_url: null, /** * An optional object with information about the children as well as about * the primary xblock type that is supported as a child. */ - 'child_info': null, + child_info: null, /** * An optional object with information about each of the ancestors. */ - 'ancestor_info': null, + ancestor_info: null, /** * Date of the last edit to this xblock or any of its descendants. */ - 'edited_on': null, + edited_on: null, /** * User who last edited the xblock or any of its descendants. Will only be present if * publishing info was explicitly requested. */ - 'edited_by': null, + edited_by: null, /** * True iff a published version of the xblock exists. */ - 'published': null, + published: null, /** * Date of the last publish of this xblock, or null if never published. */ - 'published_on': null, + published_on: null, /** * User who last published the xblock, or null if never published. Will only be present if * publishing info was explicitly requested. */ - 'published_by': null, + published_by: null, /** * True if the xblock is a parentable xblock. */ @@ -58,108 +58,108 @@ function(Backbone, _, str, ModuleUtils) { * Note: this is not always provided as a performance optimization. It is only provided for * verticals functioning as units. */ - 'has_changes': null, + has_changes: null, /** * Represents the possible publish states for an xblock. See the documentation * for XBlockVisibility to see a comprehensive enumeration of the states. */ - 'visibility_state': null, + visibility_state: null, /** * True if the release date of the xblock is in the past. */ - 'released_to_students': null, + released_to_students: null, /** * If the xblock is published, the date on which it will be released to students. * This can be null if the release date is unscheduled. */ - 'release_date': null, + release_date: null, /** * The xblock which is determining the release date. For instance, for a unit, * this will either be the parent subsection or the grandparent section. * This can be null if the release date is unscheduled. Will only be present if * publishing info was explicitly requested. */ - 'release_date_from': null, + release_date_from: null, /** * True if this xblock is currently visible to students. This is computed server-side * so that the logic isn't duplicated on the client. Will only be present if * publishing info was explicitly requested. */ - 'currently_visible_to_students': null, + currently_visible_to_students: null, /** * If xblock is graded, the date after which student assessment will be evaluated. * It has same format as release date, for example: 'Jan 02, 2015 at 00:00 UTC'. */ - 'due_date': null, + due_date: null, /** * Grading policy for xblock. */ - 'format': null, + format: null, /** * List of course graders names. */ - 'course_graders': null, + course_graders: null, /** * True if this xblock contributes to the final course grade. */ - 'graded': null, + graded: null, /** * The same as `release_date` but as an ISO-formatted date string. */ - 'start': null, + start: null, /** * The same as `due_date` but as an ISO-formatted date string. */ - 'due': null, + due: null, /** * True iff this xblock is explicitly staff locked. */ - 'has_explicit_staff_lock': null, + has_explicit_staff_lock: null, /** * True iff this any of this xblock's ancestors are staff locked. */ - 'ancestor_has_staff_lock': null, + ancestor_has_staff_lock: null, /** * The xblock which is determining the staff lock value. For instance, for a unit, * this will either be the parent subsection or the grandparent section. * This can be null if the xblock has no inherited staff lock. Will only be present if * publishing info was explicitly requested. */ - 'staff_lock_from': null, + staff_lock_from: null, /** * True iff this xblock should display a "Contains staff only content" message. */ - 'staff_only_message': null, + staff_only_message: null, /** * True iff this xblock is a unit, and it has children that are only visible to certain - * content groups. Note that this is not a recursive property. Will only be present if + * user partition groups. Note that this is not a recursive property. Will only be present if * publishing info was explicitly requested. */ - 'has_content_group_components': null, + has_partition_group_components: null, /** * actions defines the state of delete, drag and child add functionality for a xblock. * currently, each xblock has default value of 'True' for keys: deletable, draggable and childAddable. */ - 'actions': null, + actions: null, /** * Header visible to UI. */ - 'is_header_visible': null, + is_header_visible: null, /** * Optional explanatory message about the xblock. */ - 'explanatory_message': null, + explanatory_message: null, /** * The XBlock's group access rules. This is a dictionary keyed to user partition IDs, * where the values are lists of group IDs. */ - 'group_access': null, + group_access: null, /** * User partition dictionary. This is pre-processed by Studio, so it contains * some additional fields that are not stored in the course descriptor * (for example, which groups are selected for this particular XBlock). */ - 'user_partitions': null + user_partitions: null }, initialize: function() { diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js index 13bfc8d3e2..cc1d6cf8c5 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -109,15 +109,15 @@ define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/sp expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss); }); - it('updates when has_content_group_components attribute changes', function() { + it('updates when has_partition_group_components attribute changes', function() { renderContainerPage(this, mockContainerXBlockHtml); - fetch({has_content_group_components: false}); + fetch({has_partition_group_components: false}); expect(containerPage.$(visibilityNoteCss).length).toBe(0); - fetch({has_content_group_components: true}); + fetch({has_partition_group_components: true}); expect(containerPage.$(visibilityNoteCss).length).toBe(1); - fetch({has_content_group_components: false}); + fetch({has_partition_group_components: false}); expect(containerPage.$(visibilityNoteCss).length).toBe(0); }); }); diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index ff49fd0060..f811fc4bd3 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -1452,6 +1452,19 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j // Note: most tests for units can be found in Bok Choy describe('Unit', function() { + var getUnitStatus = function(options) { + mockCourseJSON = createMockCourseJSON({}, [ + createMockSectionJSON({}, [ + createMockSubsectionJSON({}, [ + createMockVerticalJSON(options) + ]) + ]) + ]); + createCourseOutlinePage(this, mockCourseJSON); + expandItemsAndVerifyState('subsection'); + return getItemsOfType('unit').find('.unit-status .status-message'); + }; + it('can be deleted', function() { var promptSpy = EditHelpers.createPromptSpy(); createCourseOutlinePage(this, mockCourseJSON); @@ -1473,6 +1486,27 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j expect(unitAnchor.attr('href')).toBe('/container/mock-unit'); }); + it('shows partition group information', function() { + var messages = getUnitStatus({has_partition_group_components: true}); + expect(messages.length).toBe(1); + expect(messages).toContainText( + 'Some content in this unit is visible only to specific groups of learners' + ); + }); + + it('does not show partition group information if visible to all', function() { + var messages = getUnitStatus({}); + expect(messages.length).toBe(0); + }); + + it('does not show partition group information if staff locked', function() { + var messages = getUnitStatus( + {has_partition_group_components: true, staff_only_message: true} + ); + expect(messages.length).toBe(1); + expect(messages).toContainText('Contains staff only content'); + }); + verifyTypePublishable('unit', function(options) { return createMockCourseJSON({}, [ createMockSectionJSON({}, [ diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index 0a0e028b23..78a9670c59 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -2,8 +2,8 @@ * Subviews (usually small side panels) for XBlockContainerPage. */ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils', - 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils'], - function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils) { + 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils'], + function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils) { 'use strict'; var disabledCss = 'is-disabled'; @@ -43,9 +43,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo }, render: function() { - this.$el.html(this.template({ - currentlyVisibleToStudents: this.model.get('currently_visible_to_students') - })); + HtmlUtils.setHtml( + this.$el, + HtmlUtils.HTML( + this.template({currentlyVisibleToStudents: this.model.get('currently_visible_to_students')}) + ) + ); return this; } }); @@ -95,30 +98,38 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo onSync: function(model) { if (ViewUtils.hasChangedAttributes(model, [ 'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state', - 'has_explicit_staff_lock', 'has_content_group_components' + 'has_explicit_staff_lock', 'has_partition_group_components' ])) { this.render(); } }, render: function() { - this.$el.html(this.template({ - visibilityState: this.model.get('visibility_state'), - visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(this.model.get('visibility_state')), - hasChanges: this.model.get('has_changes'), - editedOn: this.model.get('edited_on'), - editedBy: this.model.get('edited_by'), - published: this.model.get('published'), - publishedOn: this.model.get('published_on'), - publishedBy: this.model.get('published_by'), - released: this.model.get('released_to_students'), - releaseDate: this.model.get('release_date'), - releaseDateFrom: this.model.get('release_date_from'), - hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'), - staffLockFrom: this.model.get('staff_lock_from'), - hasContentGroupComponents: this.model.get('has_content_group_components'), - course: window.course - })); + HtmlUtils.setHtml( + this.$el, + HtmlUtils.HTML( + this.template({ + visibilityState: this.model.get('visibility_state'), + visibilityClass: XBlockViewUtils.getXBlockVisibilityClass( + this.model.get('visibility_state') + ), + hasChanges: this.model.get('has_changes'), + editedOn: this.model.get('edited_on'), + editedBy: this.model.get('edited_by'), + published: this.model.get('published'), + publishedOn: this.model.get('published_on'), + publishedBy: this.model.get('published_by'), + released: this.model.get('released_to_students'), + releaseDate: this.model.get('release_date'), + releaseDateFrom: this.model.get('release_date_from'), + hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'), + staffLockFrom: this.model.get('staff_lock_from'), + hasPartitionGroupComponents: this.model.get('has_partition_group_components'), + course: window.course, + HtmlUtils: HtmlUtils + }) + ) + ); return this; }, @@ -243,11 +254,16 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo }, render: function() { - this.$el.html(this.template({ - published: this.model.get('published'), - published_on: this.model.get('published_on'), - published_by: this.model.get('published_by') - })); + HtmlUtils.setHtml( + this.$el, + HtmlUtils.HTML( + this.template({ + published: this.model.get('published'), + published_on: this.model.get('published_on'), + published_by: this.model.get('published_by') + }) + ) + ); return this; } diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index af82c1c87f..6c5abbf767 100644 --- a/cms/static/sass/elements/_modules.scss +++ b/cms/static/sass/elements/_modules.scss @@ -499,13 +499,13 @@ $outline-indent-width: $baseline; } // status - message - .status-message { + .status-messages { margin-top: ($baseline/2); border-top: 1px solid $gray-l4; padding-top: ($baseline/4); .icon { - margin-right: ($baseline/4); + @include margin-right($baseline/4); } } diff --git a/cms/static/sass/elements/_xblocks.scss b/cms/static/sass/elements/_xblocks.scss index fe789bfe2f..b162ce567e 100644 --- a/cms/static/sass/elements/_xblocks.scss +++ b/cms/static/sass/elements/_xblocks.scss @@ -13,6 +13,8 @@ // * +Editing - Xblocks // * +Case - Special Xblock Type Overrides +@import 'edx-pattern-library-shims/base/variables'; + // +Layout - Xblocks // ==================== @@ -37,23 +39,29 @@ min-height: ($baseline*2.5); background-color: $gray-l6; padding: ($baseline/2) ($baseline/2) ($baseline/2) ($baseline); + display: flex; + align-items: center; .header-details { @extend %cont-truncated; - display: inline-block; width: 50%; vertical-align: middle; .xblock-display-name { - display: inline-block; - vertical-align: middle; + @extend %t-copy-lead1; + font-weight: font-weight(semi-bold); + } + + .xblock-group-visibility-label { + @extend %t-copy-sub1; + white-space: normal; + font-weight: font-weight(semi-bold); + color: $gray; } } .header-actions { - display: inline-block; width: 49%; - vertical-align: middle; @include text-align(right); } } diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index 4e1418a658..4bc6ab6fcc 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -3,9 +3,27 @@ var releasedToStudents = xblockInfo.get('released_to_students'); var visibilityState = xblockInfo.get('visibility_state'); var published = xblockInfo.get('published'); var prereq = xblockInfo.get('prereq'); +var hasPartitionGroups = xblockInfo.get('has_partition_group_components'); -var statusMessage = null; +var statusMessages = []; +var messageType; +var messageText; var statusType = null; +var addStatusMessage = function (statusType, message) { + var statusIconClass = ''; + if (statusType === 'warning') { + statusIconClass = 'fa-file-o'; + } else if (statusType === 'error') { + statusIconClass = 'fa-warning'; + } else if (statusType === 'staff-only' || statusType === 'gated') { + statusIconClass = 'fa-lock'; + } else if (statusType === 'partition-groups') { + statusIconClass = 'fa-eye'; + } + + statusMessages.push({iconClass: statusIconClass, text: message}); +}; + if (prereq) { var prereqDisplayName = ''; _.each(xblockInfo.get('prereqs'), function (p) { @@ -14,38 +32,37 @@ if (prereq) { return false; } }); - statusType = 'gated'; - statusMessage = interpolate( + messageType = 'gated'; + messageText = interpolate( gettext('Prerequisite: %(prereq_display_name)s'), {prereq_display_name: prereqDisplayName}, true ); + addStatusMessage(messageType, messageText); } if (staffOnlyMessage) { - statusType = 'staff-only'; - statusMessage = gettext('Contains staff only content'); -} else if (visibilityState === 'needs_attention') { - if (xblockInfo.isVertical()) { - statusType = 'warning'; + messageType = 'staff-only'; + messageText = gettext('Contains staff only content'); + addStatusMessage(messageType, messageText); +} else { + if (visibilityState === 'needs_attention' && xblockInfo.isVertical()) { + messageType = 'warning'; if (published && releasedToStudents) { - statusMessage = gettext('Unpublished changes to live content'); + messageText = gettext('Unpublished changes to live content'); } else if (!published) { - statusMessage = gettext('Unpublished units will not be released'); + messageText = gettext('Unpublished units will not be released'); } else { - statusMessage = gettext('Unpublished changes to content that will release in the future'); + messageText = gettext('Unpublished changes to content that will release in the future'); } + addStatusMessage(messageType, messageText); } -} -var statusIconClass = ''; -if (statusType === 'warning') { - statusIconClass = 'fa-file-o'; -} else if (statusType === 'error') { - statusIconClass = 'fa-warning'; -} else if (statusType === 'staff-only') { - statusIconClass = 'fa-lock'; -} else if (statusType === 'gated') { - statusIconClass = 'fa-lock'; + if (hasPartitionGroups) { + addStatusMessage( + 'partition-groups', + gettext('Some content in this unit is visible only to specific groups of learners') + ); + } } var gradingType = gettext('Ungraded'); @@ -211,11 +228,15 @@ if (is_proctored_exam) {

<% } %> - <% if (statusMessage) { %> -
- -

<%- statusMessage %>

-
+ <% if (statusMessages.length > 0) { %> +
+ <% for (var i=0; i +
+ +

<%- statusMessages[i].text %>

+
+ <% } %> +
<% } %> <% } %> diff --git a/cms/templates/js/publish-xblock.underscore b/cms/templates/js/publish-xblock.underscore index cab0f43246..203c9c3825 100644 --- a/cms/templates/js/publish-xblock.underscore +++ b/cms/templates/js/publish-xblock.underscore @@ -26,16 +26,30 @@ var visibleToStaffOnly = visibilityState === 'staff_only';

- <% if (hasChanges && editedOn && editedBy) { - var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %> - <%= interpolate(_.escape(message), { - last_saved_date: '' + _.escape(editedOn) + '', - edit_username: '' + _.escape(editedBy) + '' }, true) %> - <% } else if (publishedOn && publishedBy) { - var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); %> - <%= interpolate(_.escape(message), { - last_published_date: '' + _.escape(publishedOn) + '', - publish_username: '' + _.escape(publishedBy) + '' }, true) %> + <% if (hasChanges && editedOn && editedBy) { %> + <%= HtmlUtils.interpolateHtml( + gettext("Draft saved on {lastSavedStart}{editedOn}{lastSavedEnd} by {editedByStart}{editedBy}{editedByEnd}"), + { + lastSavedStart: HtmlUtils.HTML(''), + editedOn: editedOn, + lastSavedEnd: HtmlUtils.HTML(''), + editedByStart: HtmlUtils.HTML(''), + editedBy: editedBy, + editedByEnd: HtmlUtils.HTML('') + } + ) %> + <% } else if (publishedOn && publishedBy) { %> + <%= HtmlUtils.interpolateHtml( + gettext("Last published {lastPublishedStart}{publishedOn}{lastPublishedEnd} by {publishedByStart}{publishedBy}{publishedByEnd}"), + { + lastPublishedStart: HtmlUtils.HTML(''), + publishedOn: publishedOn, + lastPublishedEnd: HtmlUtils.HTML(''), + publishedByStart: HtmlUtils.HTML(''), + publishedBy: publishedBy, + publishedByEnd: HtmlUtils.HTML('') + } + ) %> <% } else { %> <%- gettext("Previously published") %> <% } %> @@ -83,7 +97,7 @@ var visibleToStaffOnly = visibilityState === 'staff_only'; <% } else { %>

<%- gettext("Staff and Learners") %>

<% } %> - <% if (hasContentGroupComponents) { %> + <% if (hasPartitionGroupComponents) { %>

<%- gettext("Some content in this unit is visible only to specific groups of learners.") %> diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html index 969e4e7af7..6cba8735dc 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -1,7 +1,8 @@ +<%page expression_filter="h"/> <%! from django.utils.translation import ugettext as _ from contentstore.views.helpers import xblock_studio_url -from contentstore.utils import is_visible_to_specific_content_groups +from contentstore.utils import is_visible_to_specific_partition_groups from openedx.core.djangolib.js_utils import ( dump_js_escaped_json, js_escaped_string ) @@ -36,13 +37,13 @@ messages = xblock.validate().to_json() % if not is_root: % if is_reorderable: -

  • +
  • % else: -
    +
    % endif
    @@ -61,7 +62,12 @@ messages = xblock.validate().to_json() ${_('Expand or Collapse')} % endif - ${label | h} +
    + ${label} + % if selected_groups_label: +

    ${selected_groups_label}

    + % endif +
      @@ -128,7 +134,7 @@ messages = xblock.validate().to_json()
    % if not is_root: -
    +
    % if xblock_url:
    ${_('This block contains multiple components.')}
    @@ -147,17 +153,17 @@ messages = xblock.validate().to_json() % if is_root: -
    +
    % endif % if show_preview: % if is_root or not xblock_url:
    - ${content} + ${content | n, decode.utf8}
    % else:
    - ${content} + ${content | n, decode.utf8}
    % endif % endif diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 092346967c..bf0c89487f 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -524,6 +524,15 @@ class XBlockWrapper(PageObject): """ return self.q(css=self._bounded_selector('.move-button')).is_present() + @property + def get_partition_group_message(self): + """ + Returns the message about user partition group visibility, shown under the display name + (if not present, returns None). + """ + message = self.q(css=self._bounded_selector('.xblock-group-visibility-label')) + return None if len(message) == 0 else message.first.text[0] + def go_to_container(self): """ Open the container page linked to by this xblock, and return diff --git a/common/test/acceptance/tests/studio/test_studio_container.py b/common/test/acceptance/tests/studio/test_studio_container.py index ceb09c4e51..4b1340970d 100644 --- a/common/test/acceptance/tests/studio/test_studio_container.py +++ b/common/test/acceptance/tests/studio/test_studio_container.py @@ -644,10 +644,22 @@ class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest): {'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [2]}} # "2" is Verified ) + def verify_component_group_visibility_messsage(self, component, expected_groups): + """ + Verifies that the group visibility message below the component display name is correct. + """ + if not expected_groups: + self.assertIsNone(component.get_partition_group_message) + else: + self.assertEqual("Visible to: " + expected_groups, component.get_partition_group_message) + def test_setting_enrollment_tracks(self): """ Test that enrollment track groups can be selected. """ + # Verify that the "Verified" Group is shown on the unit page (under the unit display name). + self.verify_component_group_visibility_messsage(self.html_component, "Verified Track") + # Open dialog with "Verified" already selected. visibility_editor = self.edit_component_visibility(self.html_component) self.verify_current_groups_message(visibility_editor, self.VERIFIED_TRACK) @@ -661,10 +673,12 @@ class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest): # Select "All Learners and Staff". The helper method saves the change, # then reopens the dialog to verify that it was persisted. self.select_and_verify_saved(self.html_component, self.ALL_LEARNERS_AND_STAFF) + self.verify_component_group_visibility_messsage(self.html_component, None) # Select "Audit" enrollment track. The helper method saves the change, # then reopens the dialog to verify that it was persisted. self.select_and_verify_saved(self.html_component, self.ENROLLMENT_TRACK_PARTITION, [self.AUDIT_TRACK]) + self.verify_component_group_visibility_messsage(self.html_component, "Audit Track") @attr(shard=1)