Commit 6e416310 by cahrens

Show messages about component visibility.

TNL-6746
parent be5c4fad
......@@ -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):
......
......@@ -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
......
......@@ -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:
......
......@@ -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)
}
......
......@@ -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() {
......
......@@ -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);
});
});
......
......@@ -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({}, [
......
......@@ -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;
}
......
......@@ -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);
}
}
......
......@@ -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);
}
}
......
......@@ -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) {
</p>
</div>
<% } %>
<% if (statusMessage) { %>
<div class="status-message">
<span class="icon fa <%- statusIconClass %>" aria-hidden="true"></span>
<p class="status-message-copy"><%- statusMessage %></p>
</div>
<% if (statusMessages.length > 0) { %>
<div class="status-messages">
<% for (var i=0; i<statusMessages.length; i++) { %>
<div class="status-message">
<span class="icon fa <%- statusMessages[i].iconClass %>" aria-hidden="true"></span>
<p class="status-message-copy"><%- statusMessages[i].text %></p>
</div>
<% } %>
</div>
<% } %>
</div>
<% } %>
......
......@@ -26,16 +26,30 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<div class="wrapper-last-draft bar-mod-content">
<p class="copy meta">
<% if (hasChanges && editedOn && editedBy) {
var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %>
<%= interpolate(_.escape(message), {
last_saved_date: '<span class="date">' + _.escape(editedOn) + '</span>',
edit_username: '<span class="user">' + _.escape(editedBy) + '</span>' }, true) %>
<% } else if (publishedOn && publishedBy) {
var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); %>
<%= interpolate(_.escape(message), {
last_published_date: '<span class="date">' + _.escape(publishedOn) + '</span>',
publish_username: '<span class="user">' + _.escape(publishedBy) + '</span>' }, true) %>
<% if (hasChanges && editedOn && editedBy) { %>
<%= HtmlUtils.interpolateHtml(
gettext("Draft saved on {lastSavedStart}{editedOn}{lastSavedEnd} by {editedByStart}{editedBy}{editedByEnd}"),
{
lastSavedStart: HtmlUtils.HTML('<span class="date">'),
editedOn: editedOn,
lastSavedEnd: HtmlUtils.HTML('</span>'),
editedByStart: HtmlUtils.HTML('<span class="user">'),
editedBy: editedBy,
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 { %>
<%- gettext("Previously published") %>
<% } %>
......@@ -83,7 +97,7 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<% } else { %>
<p class="visbility-copy copy"><%- gettext("Staff and Learners") %></p>
<% } %>
<% if (hasContentGroupComponents) { %>
<% if (hasPartitionGroupComponents) { %>
<p class="note-visibility">
<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>
......
<%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:
<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:
<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
<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
% endif
">
......@@ -61,7 +62,12 @@ messages = xblock.validate().to_json()
<span class="sr">${_('Expand or Collapse')}</span>
</a>
% 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 class="header-actions">
<ul class="actions-list">
......@@ -128,7 +134,7 @@ messages = xblock.validate().to_json()
</div>
</div>
% 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:
<div class="xblock-header-secondary">
<div class="meta-info">${_('This block contains multiple components.')}</div>
......@@ -147,17 +153,17 @@ messages = xblock.validate().to_json()
</header>
% 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
% if show_preview:
% if is_root or not xblock_url:
<article class="xblock-render">
${content}
${content | n, decode.utf8}
</article>
% else:
<div class="xblock-message-area">
${content}
${content | n, decode.utf8}
</div>
% endif
% endif
......
......@@ -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
......
......@@ -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)
......
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