Commit 6e416310 by cahrens

Show messages about component visibility.

TNL-6746
parent be5c4fad
...@@ -391,8 +391,8 @@ class GroupVisibilityTest(CourseTestCase): ...@@ -391,8 +391,8 @@ class GroupVisibilityTest(CourseTestCase):
def verify_all_components_visible_to_all(): # pylint: disable=invalid-name def verify_all_components_visible_to_all(): # pylint: disable=invalid-name
""" Verifies when group_access has not been set on anything. """ """ Verifies when group_access has not been set on anything. """
for item in (self.sequential, self.vertical, self.html, self.problem): 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.has_children_visible_to_specific_partition_groups(item))
self.assertFalse(utils.is_visible_to_specific_content_groups(item)) self.assertFalse(utils.is_visible_to_specific_partition_groups(item))
verify_all_components_visible_to_all() verify_all_components_visible_to_all()
...@@ -409,16 +409,16 @@ class GroupVisibilityTest(CourseTestCase): ...@@ -409,16 +409,16 @@ class GroupVisibilityTest(CourseTestCase):
self.set_group_access(self.vertical, {1: []}) self.set_group_access(self.vertical, {1: []})
self.set_group_access(self.problem, {2: [3, 4]}) self.set_group_access(self.problem, {2: [3, 4]})
# Note that "has_children_visible_to_specific_content_groups" only checks immediate children. # Note that "has_children_visible_to_specific_partition_groups" only checks immediate children.
self.assertFalse(utils.has_children_visible_to_specific_content_groups(self.sequential)) self.assertFalse(utils.has_children_visible_to_specific_partition_groups(self.sequential))
self.assertTrue(utils.has_children_visible_to_specific_content_groups(self.vertical)) self.assertTrue(utils.has_children_visible_to_specific_partition_groups(self.vertical))
self.assertFalse(utils.has_children_visible_to_specific_content_groups(self.html)) self.assertFalse(utils.has_children_visible_to_specific_partition_groups(self.html))
self.assertFalse(utils.has_children_visible_to_specific_content_groups(self.problem)) self.assertFalse(utils.has_children_visible_to_specific_partition_groups(self.problem))
self.assertTrue(utils.is_visible_to_specific_content_groups(self.sequential)) self.assertTrue(utils.is_visible_to_specific_partition_groups(self.sequential))
self.assertFalse(utils.is_visible_to_specific_content_groups(self.vertical)) self.assertFalse(utils.is_visible_to_specific_partition_groups(self.vertical))
self.assertFalse(utils.is_visible_to_specific_content_groups(self.html)) self.assertFalse(utils.is_visible_to_specific_partition_groups(self.html))
self.assertTrue(utils.is_visible_to_specific_content_groups(self.problem)) self.assertTrue(utils.is_visible_to_specific_partition_groups(self.problem))
class GetUserPartitionInfoTest(ModuleStoreTestCase): class GetUserPartitionInfoTest(ModuleStoreTestCase):
......
...@@ -163,24 +163,24 @@ def is_currently_visible_to_students(xblock): ...@@ -163,24 +163,24 @@ def is_currently_visible_to_students(xblock):
return True 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). Note that this method is not recursive (it does not check grandchildren).
""" """
if not xblock.has_children: if not xblock.has_children:
return False return False
for child in xblock.get_children(): 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 True
return False 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: if not xblock.group_access:
return False return False
......
...@@ -28,7 +28,7 @@ from xblock_django.user_service import DjangoXBlockUserService ...@@ -28,7 +28,7 @@ from xblock_django.user_service import DjangoXBlockUserService
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
from contentstore.utils import ( from contentstore.utils import (
find_release_date_source, find_staff_lock_source, is_currently_visible_to_students, 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, get_user_partition_info, get_split_group_display_name,
) )
from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \ 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 ...@@ -1005,6 +1005,7 @@ def _get_module_info(xblock, rewrite_static_links=True, include_ancestor_info=Fa
) )
if include_publishing_info: if include_publishing_info:
add_container_page_publishing_info(xblock, xblock_info) add_container_page_publishing_info(xblock, xblock_info)
return xblock_info return xblock_info
...@@ -1217,6 +1218,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -1217,6 +1218,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
) )
else: else:
xblock_info['staff_only_message'] = False xblock_info['staff_only_message'] = False
xblock_info["has_partition_group_components"] = has_children_visible_to_specific_partition_groups(
xblock
)
return xblock_info return xblock_info
...@@ -1245,7 +1250,7 @@ def add_container_page_publishing_info(xblock, xblock_info): # pylint: disable= ...@@ -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["edited_by"] = safe_get_username(xblock.subtree_edited_by)
xblock_info["published_by"] = safe_get_username(xblock.published_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["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"]: if xblock_info["release_date"]:
xblock_info["release_date_from"] = _get_release_date_from(xblock) xblock_info["release_date_from"] = _get_release_date_from(xblock)
if xblock_info["visibility_state"] == VisibilityState.staff_only: if xblock_info["visibility_state"] == VisibilityState.staff_only:
......
...@@ -7,6 +7,7 @@ from django.conf import settings ...@@ -7,6 +7,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from openedx.core.lib.xblock_utils import ( from openedx.core.lib.xblock_utils import (
...@@ -38,6 +39,7 @@ import static_replace ...@@ -38,6 +39,7 @@ import static_replace
from .session_kv_store import SessionKeyValueStore from .session_kv_store import SessionKeyValueStore
from .helpers import render_from_lms from .helpers import render_from_lms
from contentstore.utils import get_visibility_partition_info
from contentstore.views.access import get_user_role from contentstore.views.access import get_user_role
from xblock_config.models import StudioConfig from xblock_config.models import StudioConfig
...@@ -279,6 +281,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): ...@@ -279,6 +281,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
root_xblock = context.get('root_xblock') root_xblock = context.get('root_xblock')
is_root = root_xblock and xblock.location == root_xblock.location is_root = root_xblock and xblock.location == root_xblock.location
is_reorderable = _is_xblock_reorderable(xblock, context) 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 = { template_context = {
'xblock_context': context, 'xblock_context': context,
'xblock': xblock, 'xblock': xblock,
...@@ -288,6 +293,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): ...@@ -288,6 +293,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'is_reorderable': is_reorderable, 'is_reorderable': is_reorderable,
'can_edit': context.get('can_edit', True), 'can_edit': context.get('can_edit', True),
'can_edit_visibility': context.get('can_edit_visibility', True), 'can_edit_visibility': context.get('can_edit_visibility', True),
'selected_groups_label': selected_groups_label,
'can_add': context.get('can_add', True), 'can_add': context.get('can_add', True),
'can_move': context.get('can_move', True) 'can_move': context.get('can_move', True)
} }
......
...@@ -9,46 +9,46 @@ function(Backbone, _, str, ModuleUtils) { ...@@ -9,46 +9,46 @@ function(Backbone, _, str, ModuleUtils) {
// NOTE: 'publish' is not an attribute on XBlockInfo, but it is used to signal the publish // 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. // and discard changes actions. Therefore 'publish' cannot be introduced as an attribute.
defaults: { defaults: {
'id': null, id: null,
'display_name': null, display_name: null,
'category': null, category: null,
'data': null, data: null,
'metadata': null, metadata: null,
/** /**
* The Studio URL for this xblock, or null if it doesn't have one. * 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 * An optional object with information about the children as well as about
* the primary xblock type that is supported as a child. * 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. * 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. * 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 * User who last edited the xblock or any of its descendants. Will only be present if
* publishing info was explicitly requested. * publishing info was explicitly requested.
*/ */
'edited_by': null, edited_by: null,
/** /**
* True iff a published version of the xblock exists. * 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. * 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 * User who last published the xblock, or null if never published. Will only be present if
* publishing info was explicitly requested. * publishing info was explicitly requested.
*/ */
'published_by': null, published_by: null,
/** /**
* True if the xblock is a parentable xblock. * True if the xblock is a parentable xblock.
*/ */
...@@ -58,108 +58,108 @@ function(Backbone, _, str, ModuleUtils) { ...@@ -58,108 +58,108 @@ function(Backbone, _, str, ModuleUtils) {
* Note: this is not always provided as a performance optimization. It is only provided for * Note: this is not always provided as a performance optimization. It is only provided for
* verticals functioning as units. * verticals functioning as units.
*/ */
'has_changes': null, has_changes: null,
/** /**
* Represents the possible publish states for an xblock. See the documentation * Represents the possible publish states for an xblock. See the documentation
* for XBlockVisibility to see a comprehensive enumeration of the states. * 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. * 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. * 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. * 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, * 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 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 * This can be null if the release date is unscheduled. Will only be present if
* publishing info was explicitly requested. * 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 * 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 * so that the logic isn't duplicated on the client. Will only be present if
* publishing info was explicitly requested. * 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. * 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'. * 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. * Grading policy for xblock.
*/ */
'format': null, format: null,
/** /**
* List of course graders names. * List of course graders names.
*/ */
'course_graders': null, course_graders: null,
/** /**
* True if this xblock contributes to the final course grade. * 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. * 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. * The same as `due_date` but as an ISO-formatted date string.
*/ */
'due': null, due: null,
/** /**
* True iff this xblock is explicitly staff locked. * 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. * 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, * 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 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 * This can be null if the xblock has no inherited staff lock. Will only be present if
* publishing info was explicitly requested. * 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. * 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 * 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. * 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. * 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. * currently, each xblock has default value of 'True' for keys: deletable, draggable and childAddable.
*/ */
'actions': null, actions: null,
/** /**
* Header visible to UI. * Header visible to UI.
*/ */
'is_header_visible': null, is_header_visible: null,
/** /**
* Optional explanatory message about the xblock. * 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, * The XBlock's group access rules. This is a dictionary keyed to user partition IDs,
* where the values are lists of group 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 * User partition dictionary. This is pre-processed by Studio, so it contains
* some additional fields that are not stored in the course descriptor * some additional fields that are not stored in the course descriptor
* (for example, which groups are selected for this particular XBlock). * (for example, which groups are selected for this particular XBlock).
*/ */
'user_partitions': null user_partitions: null
}, },
initialize: function() { initialize: function() {
......
...@@ -109,15 +109,15 @@ define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/sp ...@@ -109,15 +109,15 @@ define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/sp
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss); 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); renderContainerPage(this, mockContainerXBlockHtml);
fetch({has_content_group_components: false}); fetch({has_partition_group_components: false});
expect(containerPage.$(visibilityNoteCss).length).toBe(0); expect(containerPage.$(visibilityNoteCss).length).toBe(0);
fetch({has_content_group_components: true}); fetch({has_partition_group_components: true});
expect(containerPage.$(visibilityNoteCss).length).toBe(1); expect(containerPage.$(visibilityNoteCss).length).toBe(1);
fetch({has_content_group_components: false}); fetch({has_partition_group_components: false});
expect(containerPage.$(visibilityNoteCss).length).toBe(0); expect(containerPage.$(visibilityNoteCss).length).toBe(0);
}); });
}); });
......
...@@ -1452,6 +1452,19 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -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 // Note: most tests for units can be found in Bok Choy
describe('Unit', function() { 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() { it('can be deleted', function() {
var promptSpy = EditHelpers.createPromptSpy(); var promptSpy = EditHelpers.createPromptSpy();
createCourseOutlinePage(this, mockCourseJSON); createCourseOutlinePage(this, mockCourseJSON);
...@@ -1473,6 +1486,27 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -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'); 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) { verifyTypePublishable('unit', function(options) {
return createMockCourseJSON({}, [ return createMockCourseJSON({}, [
createMockSectionJSON({}, [ createMockSectionJSON({}, [
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
* Subviews (usually small side panels) for XBlockContainerPage. * Subviews (usually small side panels) for XBlockContainerPage.
*/ */
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils', define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils',
'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils'], 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils'],
function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils) { function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils) {
'use strict'; 'use strict';
var disabledCss = 'is-disabled'; var disabledCss = 'is-disabled';
...@@ -43,9 +43,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -43,9 +43,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
}, },
render: function() { render: function() {
this.$el.html(this.template({ HtmlUtils.setHtml(
currentlyVisibleToStudents: this.model.get('currently_visible_to_students') this.$el,
})); HtmlUtils.HTML(
this.template({currentlyVisibleToStudents: this.model.get('currently_visible_to_students')})
)
);
return this; return this;
} }
}); });
...@@ -95,30 +98,38 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -95,30 +98,38 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
onSync: function(model) { onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, [ if (ViewUtils.hasChangedAttributes(model, [
'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state', '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(); this.render();
} }
}, },
render: function() { render: function() {
this.$el.html(this.template({ HtmlUtils.setHtml(
visibilityState: this.model.get('visibility_state'), this.$el,
visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(this.model.get('visibility_state')), HtmlUtils.HTML(
hasChanges: this.model.get('has_changes'), this.template({
editedOn: this.model.get('edited_on'), visibilityState: this.model.get('visibility_state'),
editedBy: this.model.get('edited_by'), visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(
published: this.model.get('published'), this.model.get('visibility_state')
publishedOn: this.model.get('published_on'), ),
publishedBy: this.model.get('published_by'), hasChanges: this.model.get('has_changes'),
released: this.model.get('released_to_students'), editedOn: this.model.get('edited_on'),
releaseDate: this.model.get('release_date'), editedBy: this.model.get('edited_by'),
releaseDateFrom: this.model.get('release_date_from'), published: this.model.get('published'),
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'), publishedOn: this.model.get('published_on'),
staffLockFrom: this.model.get('staff_lock_from'), publishedBy: this.model.get('published_by'),
hasContentGroupComponents: this.model.get('has_content_group_components'), released: this.model.get('released_to_students'),
course: window.course 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; return this;
}, },
...@@ -243,11 +254,16 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -243,11 +254,16 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
}, },
render: function() { render: function() {
this.$el.html(this.template({ HtmlUtils.setHtml(
published: this.model.get('published'), this.$el,
published_on: this.model.get('published_on'), HtmlUtils.HTML(
published_by: this.model.get('published_by') this.template({
})); published: this.model.get('published'),
published_on: this.model.get('published_on'),
published_by: this.model.get('published_by')
})
)
);
return this; return this;
} }
......
...@@ -499,13 +499,13 @@ $outline-indent-width: $baseline; ...@@ -499,13 +499,13 @@ $outline-indent-width: $baseline;
} }
// status - message // status - message
.status-message { .status-messages {
margin-top: ($baseline/2); margin-top: ($baseline/2);
border-top: 1px solid $gray-l4; border-top: 1px solid $gray-l4;
padding-top: ($baseline/4); padding-top: ($baseline/4);
.icon { .icon {
margin-right: ($baseline/4); @include margin-right($baseline/4);
} }
} }
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
// * +Editing - Xblocks // * +Editing - Xblocks
// * +Case - Special Xblock Type Overrides // * +Case - Special Xblock Type Overrides
@import 'edx-pattern-library-shims/base/variables';
// +Layout - Xblocks // +Layout - Xblocks
// ==================== // ====================
...@@ -37,23 +39,29 @@ ...@@ -37,23 +39,29 @@
min-height: ($baseline*2.5); min-height: ($baseline*2.5);
background-color: $gray-l6; background-color: $gray-l6;
padding: ($baseline/2) ($baseline/2) ($baseline/2) ($baseline); padding: ($baseline/2) ($baseline/2) ($baseline/2) ($baseline);
display: flex;
align-items: center;
.header-details { .header-details {
@extend %cont-truncated; @extend %cont-truncated;
display: inline-block;
width: 50%; width: 50%;
vertical-align: middle; vertical-align: middle;
.xblock-display-name { .xblock-display-name {
display: inline-block; @extend %t-copy-lead1;
vertical-align: middle; 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 { .header-actions {
display: inline-block;
width: 49%; width: 49%;
vertical-align: middle;
@include text-align(right); @include text-align(right);
} }
} }
......
...@@ -3,9 +3,27 @@ var releasedToStudents = xblockInfo.get('released_to_students'); ...@@ -3,9 +3,27 @@ var releasedToStudents = xblockInfo.get('released_to_students');
var visibilityState = xblockInfo.get('visibility_state'); var visibilityState = xblockInfo.get('visibility_state');
var published = xblockInfo.get('published'); var published = xblockInfo.get('published');
var prereq = xblockInfo.get('prereq'); 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 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) { if (prereq) {
var prereqDisplayName = ''; var prereqDisplayName = '';
_.each(xblockInfo.get('prereqs'), function (p) { _.each(xblockInfo.get('prereqs'), function (p) {
...@@ -14,38 +32,37 @@ if (prereq) { ...@@ -14,38 +32,37 @@ if (prereq) {
return false; return false;
} }
}); });
statusType = 'gated'; messageType = 'gated';
statusMessage = interpolate( messageText = interpolate(
gettext('Prerequisite: %(prereq_display_name)s'), gettext('Prerequisite: %(prereq_display_name)s'),
{prereq_display_name: prereqDisplayName}, {prereq_display_name: prereqDisplayName},
true true
); );
addStatusMessage(messageType, messageText);
} }
if (staffOnlyMessage) { if (staffOnlyMessage) {
statusType = 'staff-only'; messageType = 'staff-only';
statusMessage = gettext('Contains staff only content'); messageText = gettext('Contains staff only content');
} else if (visibilityState === 'needs_attention') { addStatusMessage(messageType, messageText);
if (xblockInfo.isVertical()) { } else {
statusType = 'warning'; if (visibilityState === 'needs_attention' && xblockInfo.isVertical()) {
messageType = 'warning';
if (published && releasedToStudents) { if (published && releasedToStudents) {
statusMessage = gettext('Unpublished changes to live content'); messageText = gettext('Unpublished changes to live content');
} else if (!published) { } else if (!published) {
statusMessage = gettext('Unpublished units will not be released'); messageText = gettext('Unpublished units will not be released');
} else { } 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 (hasPartitionGroups) {
if (statusType === 'warning') { addStatusMessage(
statusIconClass = 'fa-file-o'; 'partition-groups',
} else if (statusType === 'error') { gettext('Some content in this unit is visible only to specific groups of learners')
statusIconClass = 'fa-warning'; );
} else if (statusType === 'staff-only') { }
statusIconClass = 'fa-lock';
} else if (statusType === 'gated') {
statusIconClass = 'fa-lock';
} }
var gradingType = gettext('Ungraded'); var gradingType = gettext('Ungraded');
...@@ -211,11 +228,15 @@ if (is_proctored_exam) { ...@@ -211,11 +228,15 @@ if (is_proctored_exam) {
</p> </p>
</div> </div>
<% } %> <% } %>
<% if (statusMessage) { %> <% if (statusMessages.length > 0) { %>
<div class="status-message"> <div class="status-messages">
<span class="icon fa <%- statusIconClass %>" aria-hidden="true"></span> <% for (var i=0; i<statusMessages.length; i++) { %>
<p class="status-message-copy"><%- statusMessage %></p> <div class="status-message">
</div> <span class="icon fa <%- statusMessages[i].iconClass %>" aria-hidden="true"></span>
<p class="status-message-copy"><%- statusMessages[i].text %></p>
</div>
<% } %>
</div>
<% } %> <% } %>
</div> </div>
<% } %> <% } %>
......
...@@ -26,16 +26,30 @@ var visibleToStaffOnly = visibilityState === 'staff_only'; ...@@ -26,16 +26,30 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<div class="wrapper-last-draft bar-mod-content"> <div class="wrapper-last-draft bar-mod-content">
<p class="copy meta"> <p class="copy meta">
<% if (hasChanges && editedOn && editedBy) { <% if (hasChanges && editedOn && editedBy) { %>
var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %> <%= HtmlUtils.interpolateHtml(
<%= interpolate(_.escape(message), { gettext("Draft saved on {lastSavedStart}{editedOn}{lastSavedEnd} by {editedByStart}{editedBy}{editedByEnd}"),
last_saved_date: '<span class="date">' + _.escape(editedOn) + '</span>', {
edit_username: '<span class="user">' + _.escape(editedBy) + '</span>' }, true) %> lastSavedStart: HtmlUtils.HTML('<span class="date">'),
<% } else if (publishedOn && publishedBy) { editedOn: editedOn,
var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); %> lastSavedEnd: HtmlUtils.HTML('</span>'),
<%= interpolate(_.escape(message), { editedByStart: HtmlUtils.HTML('<span class="user">'),
last_published_date: '<span class="date">' + _.escape(publishedOn) + '</span>', editedBy: editedBy,
publish_username: '<span class="user">' + _.escape(publishedBy) + '</span>' }, true) %> editedByEnd: HtmlUtils.HTML('</span>')
}
) %>
<% } else if (publishedOn && publishedBy) { %>
<%= HtmlUtils.interpolateHtml(
gettext("Last published {lastPublishedStart}{publishedOn}{lastPublishedEnd} by {publishedByStart}{publishedBy}{publishedByEnd}"),
{
lastPublishedStart: HtmlUtils.HTML('<span class="date">'),
publishedOn: publishedOn,
lastPublishedEnd: HtmlUtils.HTML('</span>'),
publishedByStart: HtmlUtils.HTML('<span class="user">'),
publishedBy: publishedBy,
publishedByEnd: HtmlUtils.HTML('</span>')
}
) %>
<% } else { %> <% } else { %>
<%- gettext("Previously published") %> <%- gettext("Previously published") %>
<% } %> <% } %>
...@@ -83,7 +97,7 @@ var visibleToStaffOnly = visibilityState === 'staff_only'; ...@@ -83,7 +97,7 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<% } else { %> <% } else { %>
<p class="visbility-copy copy"><%- gettext("Staff and Learners") %></p> <p class="visbility-copy copy"><%- gettext("Staff and Learners") %></p>
<% } %> <% } %>
<% if (hasContentGroupComponents) { %> <% if (hasPartitionGroupComponents) { %>
<p class="note-visibility"> <p class="note-visibility">
<span class="icon fa fa-eye" aria-hidden="true"></span> <span class="icon fa fa-eye" aria-hidden="true"></span>
<span class="note-copy"><%- gettext("Some content in this unit is visible only to specific groups of learners.") %></span> <span class="note-copy"><%- gettext("Some content in this unit is visible only to specific groups of learners.") %></span>
......
<%page expression_filter="h"/>
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from contentstore.views.helpers import xblock_studio_url 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 ( from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string dump_js_escaped_json, js_escaped_string
) )
...@@ -36,13 +37,13 @@ messages = xblock.validate().to_json() ...@@ -36,13 +37,13 @@ messages = xblock.validate().to_json()
% if not is_root: % if not is_root:
% if is_reorderable: % if is_reorderable:
<li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location | h}" data-course-key="${xblock.location.course_key | h}"> <li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
% else: % else:
<div class="studio-xblock-wrapper" data-locator="${xblock.location | h}" data-course-key="${xblock.location.course_key | h}"> <div class="studio-xblock-wrapper" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
% endif % endif
<section class="wrapper-xblock ${section_class} ${collapsible_class} <section class="wrapper-xblock ${section_class} ${collapsible_class}
% if is_visible_to_specific_content_groups(xblock): % if is_visible_to_specific_partition_groups(xblock):
has-group-visibility-set has-group-visibility-set
% endif % endif
"> ">
...@@ -61,7 +62,12 @@ messages = xblock.validate().to_json() ...@@ -61,7 +62,12 @@ messages = xblock.validate().to_json()
<span class="sr">${_('Expand or Collapse')}</span> <span class="sr">${_('Expand or Collapse')}</span>
</a> </a>
% endif % endif
<span class="xblock-display-name">${label | h}</span> <div class="xblock-display-title">
<span class="xblock-display-name">${label}</span>
% if selected_groups_label:
<p class="xblock-group-visibility-label">${selected_groups_label}</p>
% endif
</div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<ul class="actions-list"> <ul class="actions-list">
...@@ -128,7 +134,7 @@ messages = xblock.validate().to_json() ...@@ -128,7 +134,7 @@ messages = xblock.validate().to_json()
</div> </div>
</div> </div>
% if not is_root: % if not is_root:
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location | h}"/> <div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location}"/>
% if xblock_url: % if xblock_url:
<div class="xblock-header-secondary"> <div class="xblock-header-secondary">
<div class="meta-info">${_('This block contains multiple components.')}</div> <div class="meta-info">${_('This block contains multiple components.')}</div>
...@@ -147,17 +153,17 @@ messages = xblock.validate().to_json() ...@@ -147,17 +153,17 @@ messages = xblock.validate().to_json()
</header> </header>
% if is_root: % if is_root:
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location | h}"/> <div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location}"/>
% endif % endif
% if show_preview: % if show_preview:
% if is_root or not xblock_url: % if is_root or not xblock_url:
<article class="xblock-render"> <article class="xblock-render">
${content} ${content | n, decode.utf8}
</article> </article>
% else: % else:
<div class="xblock-message-area"> <div class="xblock-message-area">
${content} ${content | n, decode.utf8}
</div> </div>
% endif % endif
% endif % endif
......
...@@ -524,6 +524,15 @@ class XBlockWrapper(PageObject): ...@@ -524,6 +524,15 @@ class XBlockWrapper(PageObject):
""" """
return self.q(css=self._bounded_selector('.move-button')).is_present() 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): def go_to_container(self):
""" """
Open the container page linked to by this xblock, and return Open the container page linked to by this xblock, and return
......
...@@ -644,10 +644,22 @@ class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest): ...@@ -644,10 +644,22 @@ class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest):
{'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [2]}} # "2" is Verified {'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): def test_setting_enrollment_tracks(self):
""" """
Test that enrollment track groups can be selected. 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. # Open dialog with "Verified" already selected.
visibility_editor = self.edit_component_visibility(self.html_component) visibility_editor = self.edit_component_visibility(self.html_component)
self.verify_current_groups_message(visibility_editor, self.VERIFIED_TRACK) self.verify_current_groups_message(visibility_editor, self.VERIFIED_TRACK)
...@@ -661,10 +673,12 @@ class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest): ...@@ -661,10 +673,12 @@ class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest):
# Select "All Learners and Staff". The helper method saves the change, # Select "All Learners and Staff". The helper method saves the change,
# then reopens the dialog to verify that it was persisted. # then reopens the dialog to verify that it was persisted.
self.select_and_verify_saved(self.html_component, self.ALL_LEARNERS_AND_STAFF) 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, # Select "Audit" enrollment track. The helper method saves the change,
# then reopens the dialog to verify that it was persisted. # 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.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) @attr(shard=1)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment