Merge pull request #14863 from edx/christina/tnl-6746
Show messages about component visibility.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
<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)
|
||||
|
||||
Reference in New Issue
Block a user