Add ability to set visibility by enrollment track.

TNL-6744
This commit is contained in:
cahrens
2017-03-22 14:45:59 -04:00
parent 073826cac7
commit dbae1e397d
25 changed files with 571 additions and 493 deletions

View File

@@ -8,8 +8,8 @@ from util.db import generate_int_id, MYSQL_MAX_INT
from django.utils.translation import ugettext as _
from contentstore.utils import reverse_usage_url
from xmodule.partitions.partitions import UserPartition
from xmodule.partitions.partitions_service import get_all_partitions_for_course, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions import UserPartition, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions_service import get_all_partitions_for_course
from xmodule.split_test_module import get_split_user_partitions
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
@@ -18,11 +18,11 @@ MINIMUM_GROUP_ID = MINIMUM_STATIC_PARTITION_ID
RANDOM_SCHEME = "random"
COHORT_SCHEME = "cohort"
# Note: the following content group configuration strings are not
# translated since they are not visible to users.
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = 'The groups in this configuration can be mapped to cohort groups in the LMS.'
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.'
)
CONTENT_GROUP_CONFIGURATION_NAME = 'Content Group Configuration'
CONTENT_GROUP_CONFIGURATION_NAME = _('Content Groups')
log = logging.getLogger(__name__)

View File

@@ -510,7 +510,7 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
self.assertEqual(len(groups), 3)
self.assertEqual(groups[2], {
"id": 3,
"name": "Deleted group",
"name": "Deleted Group",
"selected": True,
"deleted": True
})

View File

@@ -401,7 +401,7 @@ def get_user_partition_info(xblock, schemes=None, course=None):
for gid in missing_group_ids:
groups.append({
"id": gid,
"name": _("Deleted group"),
"name": _("Deleted Group"),
"selected": True,
"deleted": True,
})
@@ -429,30 +429,45 @@ def get_visibility_partition_info(xblock):
Returns: dict
"""
user_partitions = get_user_partition_info(xblock, schemes=["verification", "cohort"])
cohort_partitions = []
verification_partitions = []
has_selected_groups = False
selected_verified_partition_id = None
selectable_partitions = []
# We wish to display enrollment partitions before cohort partitions.
enrollment_user_partitions = get_user_partition_info(xblock, schemes=["enrollment_track"])
# Pre-process the partitions to make it easier to display the UI
for p in user_partitions:
has_selected = any(g["selected"] for g in p["groups"])
has_selected_groups = has_selected_groups or has_selected
# For enrollment partitions, we only show them if there is a selected group or
# or if the number of groups > 1.
for partition in enrollment_user_partitions:
if len(partition["groups"]) > 1 or any(group["selected"] for group in partition["groups"]):
selectable_partitions.append(partition)
if p["scheme"] == "cohort":
cohort_partitions.append(p)
elif p["scheme"] == "verification":
verification_partitions.append(p)
if has_selected:
selected_verified_partition_id = p["id"]
# Now add the cohort user partitions.
selectable_partitions = selectable_partitions + get_user_partition_info(xblock, schemes=["cohort"])
# Find the first partition with a selected group. That will be the one initially enabled in the dialog
# (if the course has only been added in Studio, only one partition should have a selected group).
selected_partition_index = -1
# At the same time, build up all the selected groups as they are displayed in the dialog title.
selected_groups_label = ''
for index, partition in enumerate(selectable_partitions):
for group in partition["groups"]:
if group["selected"]:
if len(selected_groups_label) == 0:
selected_groups_label = group['name']
else:
# Translators: This is building up a list of groups. It is marked for translation because of the
# comma, which is used as a separator between each group.
selected_groups_label = _('{previous_groups}, {current_group}').format(
previous_groups=selected_groups_label,
current_group=group['name']
)
if selected_partition_index == -1:
selected_partition_index = index
return {
"user_partitions": user_partitions,
"cohort_partitions": cohort_partitions,
"verification_partitions": verification_partitions,
"has_selected_groups": has_selected_groups,
"selected_verified_partition_id": selected_verified_partition_id,
"selectable_partitions": selectable_partitions,
"selected_partition_index": selected_partition_index,
"selected_groups_label": selected_groups_label,
}

View File

@@ -8,7 +8,7 @@ import ddt
from mock import patch
from contentstore.utils import reverse_course_url, reverse_usage_url
from contentstore.course_group_config import GroupConfiguration
from contentstore.course_group_config import GroupConfiguration, CONTENT_GROUP_CONFIGURATION_NAME
from contentstore.tests.utils import CourseTestCase
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -240,7 +240,7 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'First name')
self.assertContains(response, 'Group C')
self.assertContains(response, 'Content Group Configuration')
self.assertContains(response, CONTENT_GROUP_CONFIGURATION_NAME)
def test_unsupported_http_accept_header(self):
"""

View File

@@ -44,8 +44,9 @@ from xblock.exceptions import NoSuchHandlerError
from xblock_django.user_service import DjangoXBlockUserService
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import Location
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.partitions_service import ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions import (
Group, UserPartition, ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID
)
class AsideTest(XBlockAside):
@@ -348,9 +349,9 @@ class GetItemTest(ItemTest):
self.course.user_partitions = [
UserPartition(
id=MINIMUM_STATIC_PARTITION_ID,
name="Verification user partition",
scheme=UserPartition.get_scheme("verification"),
description="Verification user partition",
name="Random user partition",
scheme=UserPartition.get_scheme("random"),
description="Random user partition",
groups=[
Group(id=MINIMUM_STATIC_PARTITION_ID + 1, name="Group A"), # See note above.
Group(id=MINIMUM_STATIC_PARTITION_ID + 2, name="Group B"), # See note above.
@@ -370,7 +371,7 @@ class GetItemTest(ItemTest):
self.assertEqual(result["user_partitions"], [
{
"id": ENROLLMENT_TRACK_PARTITION_ID,
"name": "Enrollment Track Partition",
"name": "Enrollment Tracks",
"scheme": "enrollment_track",
"groups": [
{
@@ -383,8 +384,8 @@ class GetItemTest(ItemTest):
},
{
"id": MINIMUM_STATIC_PARTITION_ID,
"name": "Verification user partition",
"scheme": "verification",
"name": "Random user partition",
"scheme": "random",
"groups": [
{
"id": MINIMUM_STATIC_PARTITION_ID + 1,

View File

@@ -1,16 +1,33 @@
"""
Tests for the Studio authoring XBlock mixin.
"""
from django.conf import settings
from django.test.utils import override_settings
from course_modes.tests.factories import CourseModeFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.partitions import (
Group, UserPartition, ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID
)
class AuthoringMixinTestCase(ModuleStoreTestCase):
"""
Tests the studio authoring XBlock mixin.
"""
GROUP_NO_LONGER_EXISTS = "This group no longer exists"
NO_CONTENT_OR_ENROLLMENT_GROUPS = "No visibility settings are defined for this component"
NO_CONTENT_ENROLLMENT_TRACK_ENABLED = "specific groups of learners based either on their enrollment track, or by content groups that you create"
NO_CONTENT_ENROLLMENT_TRACK_DISABLED = "specific groups of learners based on content groups that you create"
CONTENT_GROUPS_TITLE = "Content Groups"
ENROLLMENT_GROUPS_TITLE = "Enrollment Tracks"
STAFF_LOCKED = 'The unit that contains this component is hidden from learners'
FEATURES_WITH_ENROLLMENT_TRACK_DISABLED = settings.FEATURES.copy()
FEATURES_WITH_ENROLLMENT_TRACK_DISABLED['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = False
def setUp(self):
"""
Create a simple course with a video component.
@@ -47,8 +64,8 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
"""
# pylint: disable=attribute-defined-outside-init
self.content_partition = UserPartition(
1,
'Content Groups',
MINIMUM_STATIC_PARTITION_ID,
self.CONTENT_GROUPS_TITLE,
'Contains Groups for Cohorted Courseware',
content_groups,
scheme_id='cohort'
@@ -56,39 +73,19 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
self.course.user_partitions = [self.content_partition]
self.store.update_item(self.course, self.user.id)
def create_verification_user_partitions(self, checkpoint_names):
"""
Create user partitions for verification checkpoints.
"""
scheme = UserPartition.get_scheme("verification")
self.course.user_partitions = [
UserPartition(
id=0,
name=checkpoint_name,
description="Verification checkpoint",
scheme=scheme,
groups=[
Group(scheme.ALLOW, "Completed verification at {}".format(checkpoint_name)),
Group(scheme.DENY, "Did not complete verification at {}".format(checkpoint_name)),
],
)
for checkpoint_name in checkpoint_names
]
self.store.update_item(self.course, self.user.id)
def set_staff_only(self, item_location):
"""Make an item visible to staff only."""
item = self.store.get_item(item_location)
item.visible_to_staff_only = True
self.store.update_item(item, self.user.id)
def set_group_access(self, item_location, group_ids):
def set_group_access(self, item_location, group_ids, partition_id=None):
"""
Set group_access for the specified item to the specified group
ids within the content partition.
"""
item = self.store.get_item(item_location)
item.group_access[self.content_partition.id] = group_ids
item.group_access[self.content_partition.id if partition_id is None else partition_id] = group_ids
self.store.update_item(item, self.user.id)
def verify_visibility_view_contains(self, item_location, substrings):
@@ -101,39 +98,70 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
for string in substrings:
self.assertIn(string, html)
def verify_visibility_view_does_not_contain(self, item_location, substrings):
"""
Verify that an item's visibility view returns an html string
that does NOT contain the provided substrings.
"""
item = self.store.get_item(item_location)
html = item.visibility_view().body_html()
for string in substrings:
self.assertNotIn(string, html)
def test_html_no_partition(self):
self.verify_visibility_view_contains(self.video_location, 'No content groups exist')
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
def test_html_empty_partition(self):
self.create_content_groups([])
self.verify_visibility_view_contains(self.video_location, 'No content groups exist')
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
def test_html_populated_partition(self):
self.create_content_groups(self.pet_groups)
self.verify_visibility_view_contains(self.video_location, ['Cat Lovers', 'Dog Lovers'])
self.verify_visibility_view_contains(
self.video_location,
[self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers']
)
self.verify_visibility_view_does_not_contain(
self.video_location,
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.ENROLLMENT_GROUPS_TITLE]
)
def test_html_no_partition_staff_locked(self):
self.set_staff_only(self.vertical_location)
self.verify_visibility_view_contains(self.video_location, ['No content groups exist'])
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
self.verify_visibility_view_does_not_contain(
self.video_location,
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, self.ENROLLMENT_GROUPS_TITLE]
)
def test_html_empty_partition_staff_locked(self):
self.create_content_groups([])
self.set_staff_only(self.vertical_location)
self.verify_visibility_view_contains(self.video_location, 'No content groups exist')
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
self.verify_visibility_view_does_not_contain(
self.video_location,
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, self.ENROLLMENT_GROUPS_TITLE]
)
def test_html_populated_partition_staff_locked(self):
self.create_content_groups(self.pet_groups)
self.set_staff_only(self.vertical_location)
self.verify_visibility_view_contains(
self.video_location,
['The Unit this component is contained in is hidden from students.', 'Cat Lovers', 'Dog Lovers']
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers']
)
def test_html_false_content_group(self):
self.create_content_groups(self.pet_groups)
self.set_group_access(self.video_location, ['false_group_id'])
self.verify_visibility_view_contains(
self.video_location, ['Cat Lovers', 'Dog Lovers', 'Content group no longer exists.']
self.video_location,
[self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers', self.GROUP_NO_LONGER_EXISTS]
)
self.verify_visibility_view_does_not_contain(
self.video_location,
[self.STAFF_LOCKED]
)
def test_html_false_content_group_staff_locked(self):
@@ -145,18 +173,84 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
[
'Cat Lovers',
'Dog Lovers',
'The Unit this component is contained in is hidden from students.',
'Content group no longer exists.'
self.STAFF_LOCKED,
self.GROUP_NO_LONGER_EXISTS
]
)
def test_html_verification_checkpoints(self):
self.create_verification_user_partitions(["Midterm A", "Midterm B"])
@override_settings(FEATURES=FEATURES_WITH_ENROLLMENT_TRACK_DISABLED)
def test_enrollment_tracks_disabled(self):
"""
Test that the "no groups" messages doesn't reference enrollment tracks if
they are disabled.
"""
self.verify_visibility_view_contains(
self.video_location,
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.NO_CONTENT_ENROLLMENT_TRACK_DISABLED]
)
self.verify_visibility_view_does_not_contain(self.video_location, [self.NO_CONTENT_ENROLLMENT_TRACK_ENABLED])
def test_enrollment_track_partitions_only(self):
"""
Test what is displayed with no content groups but 2 enrollment modes registered.
In all the cases where no enrollment modes are explicitly added, only the default
enrollment mode exists, and we do not show it as an option (unless the course staff
member has previously selected it).
"""
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
self.verify_visibility_view_contains(
self.video_location,
[self.ENROLLMENT_GROUPS_TITLE, 'audit course', 'verified course']
)
self.verify_visibility_view_does_not_contain(
self.video_location,
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.CONTENT_GROUPS_TITLE]
)
def test_enrollment_track_partitions_and_content_groups(self):
"""
Test what is displayed with both enrollment groups and content groups.
"""
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
self.create_content_groups(self.pet_groups)
self.verify_visibility_view_contains(
self.video_location,
[
"Verification Checkpoint",
"Midterm A",
"Midterm B",
self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers',
self.ENROLLMENT_GROUPS_TITLE, 'audit course', 'verified course'
]
)
self.verify_visibility_view_does_not_contain(
self.video_location,
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS]
)
def test_missing_enrollment_mode(self):
"""
Test that an enrollment mode that is no longer registered is displayed as 'deleted',
regardless of the number of current enrollment modes in the course.
"""
# Only 1 mode (the default) exists, so nothing initially shows in the visibility view.
self.verify_visibility_view_contains(
self.video_location,
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.NO_CONTENT_ENROLLMENT_TRACK_ENABLED]
)
self.verify_visibility_view_does_not_contain(
self.video_location, [self.ENROLLMENT_GROUPS_TITLE, self.GROUP_NO_LONGER_EXISTS]
)
# Set group_access to reference a missing mode.
self.set_group_access(self.video_location, ['10'], ENROLLMENT_TRACK_PARTITION_ID)
self.verify_visibility_view_contains(
self.video_location, [self.ENROLLMENT_GROUPS_TITLE, self.GROUP_NO_LONGER_EXISTS]
)
# Add 2 explicit enrollment modes.
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
self.verify_visibility_view_contains(
self.video_location,
[self.ENROLLMENT_GROUPS_TITLE, 'audit course', 'verified course', self.GROUP_NO_LONGER_EXISTS]
)

View File

@@ -256,19 +256,6 @@ function(Backbone, _, str, ModuleUtils) {
*/
isEditableOnCourseOutline: function() {
return this.isSequential() || this.isChapter() || this.isVertical();
},
/*
* Check whether any verification checkpoints are defined in the course.
* Verification checkpoints are defined if there exists a user partition
* that uses the verification partition scheme.
*/
hasVerifiedCheckpoints: function() {
var partitions = this.get('user_partitions') || [];
return Boolean(_.find(partitions, function(p) {
return p.scheme === 'verification';
}));
}
});
return XBlockInfo;

View File

@@ -15,7 +15,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'use strict';
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor, StaffLockEditor,
ContentVisibilityEditor, VerificationAccessEditor, TimedExaminationPreferenceEditor, AccessEditor;
ContentVisibilityEditor, TimedExaminationPreferenceEditor, AccessEditor;
CourseOutlineXBlockModal = BaseModal.extend({
events: _.extend({}, BaseModal.prototype.events, {
@@ -720,109 +720,6 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
});
VerificationAccessEditor = AbstractEditor.extend({
templateName: 'verification-access-editor',
className: 'edit-verification-access',
// This constant MUST match the group ID
// defined by VerificationPartitionScheme on the backend!
ALLOW_GROUP_ID: 1,
getSelectedPartition: function() {
var hasRestrictions = $('#verification-access-checkbox').is(':checked'),
selectedPartitionID = null;
if (hasRestrictions) {
selectedPartitionID = $('#verification-partition-select').val();
}
return parseInt(selectedPartitionID, 10);
},
getGroupAccess: function() {
var groupAccess = _.clone(this.model.get('group_access')) || [],
userPartitions = this.model.get('user_partitions') || [],
selectedPartition = this.getSelectedPartition(),
that = this;
// We display a simplified UI to course authors.
// On the backend, each verification checkpoint is associated
// with a user partition that has two groups. For example,
// if two checkpoints were defined, they might look like:
//
// Midterm A: |-- ALLOW --|-- DENY --|
// Midterm B: |-- ALLOW --|-- DENY --|
//
// To make life easier for course authors, we display
// *one* option for each checkpoint:
//
// [X] Must complete verification checkpoint
// Dropdown:
// * Midterm A
// * Midterm B
//
// This is where we map the simplified UI to
// the underlying user partition. If the user checked
// the box, that means there *is* a restriction,
// so only the "ALLOW" group for the selected partition has access.
// Otherwise, all groups in the partition have access.
//
_.each(userPartitions, function(partition) {
if (partition.scheme === 'verification') {
if (selectedPartition === partition.id) {
groupAccess[partition.id] = [that.ALLOW_GROUP_ID];
} else {
delete groupAccess[partition.id];
}
}
});
return groupAccess;
},
getRequestData: function() {
var groupAccess = this.getGroupAccess(),
hasChanges = !_.isEqual(groupAccess, this.model.get('group_access'));
return hasChanges ? {
publish: 'republish',
metadata: {
group_access: groupAccess
}
} : {};
},
getContext: function() {
var partitions = this.model.get('user_partitions'),
hasRestrictions = false,
verificationPartitions = [],
isSelected = false;
// Display a simplified version of verified partition schemes.
// Although there are two groups defined (ALLOW and DENY),
// we show only the ALLOW group.
// To avoid searching all the groups, we're assuming that the editor
// either sets the ALLOW group or doesn't set any groups (implicitly allow all).
_.each(partitions, function(item) {
if (item.scheme === 'verification') {
isSelected = _.any(_.pluck(item.groups, 'selected'));
hasRestrictions = hasRestrictions || isSelected;
verificationPartitions.push({
'id': item.id,
'name': item.name,
'selected': isSelected
});
}
});
return {
'hasVerificationRestrictions': hasRestrictions,
'verificationPartitions': verificationPartitions
};
}
});
return {
getModal: function(type, xblockInfo, options) {
if (type === 'edit') {
@@ -837,10 +734,6 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var editors = [];
if (xblockInfo.isVertical()) {
editors = [StaffLockEditor];
if (xblockInfo.hasVerifiedCheckpoints()) {
editors.push(VerificationAccessEditor);
}
} else {
tabs = [
{

View File

@@ -5,25 +5,26 @@
'use strict';
function VisibilityEditorView(runtime, element) {
this.getGroupAccess = function() {
var groupAccess = {},
checkboxValues,
partitionId,
groupId,
groupId;
// This constant MUST match the group ID
// defined by VerificationPartitionScheme on the backend!
ALLOW_GROUP_ID = 1;
// Get the selected user partition (only allowed to select one).
partitionId = parseInt(element.find('.partition-visibility select').val(), 10);
if (element.find('.visibility-level-all').prop('checked')) {
// "All Learners and Staff" is selected (or "Choose one", which is only shown when
// current visibility is "All Learners and Staff" at the time the dialog is opened).
if (partitionId === -1) {
return {};
}
// Cohort partitions (user is allowed to select more than one)
element.find('.field-visibility-content-group input:checked').each(function(index, input) {
checkboxValues = $(input).val().split('-');
partitionId = parseInt(checkboxValues[0], 10);
groupId = parseInt(checkboxValues[1], 10);
// Otherwise get the checked groups within the selected partition.
element.find(
'.partition-group-visibility-' + partitionId + ' input:checked'
).each(function(index, input) {
groupId = parseInt($(input).val(), 10);
if (groupAccess.hasOwnProperty(partitionId)) {
groupAccess[partitionId].push(groupId);
@@ -32,38 +33,25 @@
}
});
// Verification partitions (user can select exactly one)
if (element.find('#verification-access-checkbox').prop('checked')) {
partitionId = parseInt($('#verification-access-dropdown').val(), 10);
groupAccess[partitionId] = [ALLOW_GROUP_ID];
}
return groupAccess;
};
// When selecting "all students and staff", uncheck the specific groups
element.find('.field-visibility-level input').change(function(event) {
if ($(event.target).hasClass('visibility-level-all')) {
element.find('.field-visibility-content-group input, .field-visibility-verification input')
.prop('checked', false);
element.find('.partition-visibility select').change(function(event) {
var partitionId;
// Hide all the partition group options.
element.find('.partition-group-control').addClass('is-hidden');
// If a partition is selected, display its groups.
partitionId = parseInt($(event.target).val(), 10);
if (partitionId >= 0) {
element.find('.partition-group-control-' + partitionId).removeClass('is-hidden');
}
});
// When selecting a specific group, deselect "all students and staff" and
// select "specific content groups" instead.`
element.find('.field-visibility-content-group input, .field-visibility-verification input')
.change(function() {
element.find('.visibility-level-all').prop('checked', false);
element.find('.visibility-level-specific').prop('checked', true);
});
}
VisibilityEditorView.prototype.collectFieldData = function collectFieldData() {
return {
metadata: {
'group_access': this.getGroupAccess()
}
};
return {metadata: {group_access: this.getGroupAccess()}};
};
function initializeVisibilityEditor(runtime, element) {

View File

@@ -91,3 +91,5 @@
// CAPA Problem Feedback
@import 'edx-pattern-library-shims/buttons';
@import 'edx-pattern-library-shims/base/variables';

View File

@@ -1,6 +1,8 @@
// studio - elements - modal-window
// ========================
@import 'edx-pattern-library-shims/base/variables';
// start with the view/body
[class*="view-"] {
@@ -482,59 +484,59 @@
// MODAL TYPE: component - visibility modal
.xblock-visibility_view {
.visibility-controls-secondary {
max-height: 100%;
overflow-y: auto;
@include margin(($baseline*0.75), 0, 0, $baseline);
// We don't wish the dialog to resize for the common case of 2 groups.
min-height: 190px;
.visibility-header {
padding-bottom: $baseline;
margin-bottom: 0;
color: $gray-d3;
}
.visibility-controls-group {
@extend %wipe-last-child;
margin-bottom: $baseline;
.current-visibility-title {
font-weight: font-weight(semi-bold);
.icon {
@include margin-right($baseline/8);
}
}
.group-select-title {
font-weight: font-weight(semi-bold);
font-size: inherit;
}
.partition-visibility {
padding-top: $baseline;
}
// UI: form fields
.list-fields {
.partition-group-control {
padding-top: ($baseline/2);
.field {
@extend %wipe-last-child;
margin-bottom: ($baseline/4);
label {
@extend %t-copy-sub1;
}
}
// UI: radio and checkbox inputs
.field-radio, .field-checkbox {
margin-top: ($baseline/4);
label {
@include margin-left($baseline/4);
font-size: inherit;
}
}
}
.field-visibility-verification {
.note {
@extend %t-copy-sub2;
@extend %t-regular;
margin: 14px 0 0 24px;
display: block;
}
}
// CASE: content group has been removed
.field-visibility-content-group.was-removed {
// CASE: content or enrollment group has been removed
.partition-group-visibility.was-removed {
.input-checkbox:checked ~ label {
color: $color-error;
color: $error-color;
}
.note {
@extend %t-copy-sub2;
@extend %t-regular;
display: block;
color: $color-error;
color: $error-color;
}
}
@@ -698,7 +700,7 @@
}
// UI: staff lock section
.edit-staff-lock, .edit-settings-timed-examination, .edit-verification-access {
.edit-staff-lock, .edit-settings-timed-examination {
.checkbox-cosmetic .input-checkbox {
@extend %cont-text-sr;
@@ -730,13 +732,6 @@
}
}
.verification-access {
.checkbox-cosmetic .label {
@include float(left);
margin: 2px 6px 0 0;
}
}
// UI: timed and proctored exam section
.edit-settings-timed-examination {

View File

@@ -86,7 +86,7 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<% if (hasContentGroupComponents) { %>
<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 particular content groups") %></span>
<span class="note-copy"><%- gettext("Some content in this unit is visible only to specific groups of learners.") %></span>
</p>
<% } %>
<ul class="actions-inline">

View File

@@ -1,25 +1,29 @@
<%page expression_filter="h"/>
<%
from django.conf import settings
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.credit.partition_schemes import VerificationPartitionScheme
from contentstore.utils import ancestor_has_staff_lock, get_visibility_partition_info
from openedx.core.djangolib.markup import HTML, Text
partition_info = get_visibility_partition_info(xblock)
user_partitions = partition_info["user_partitions"]
cohort_partitions = partition_info["cohort_partitions"]
verification_partitions = partition_info["verification_partitions"]
has_selected_groups = partition_info["has_selected_groups"]
selected_verified_partition_id = partition_info["selected_verified_partition_id"]
selectable_partitions = partition_info["selectable_partitions"]
selected_partition_index = partition_info["selected_partition_index"]
selected_groups_label = partition_info["selected_groups_label"]
is_staff_locked = ancestor_has_staff_lock(xblock)
%>
<div class="modal-section visibility-summary">
% if len(user_partitions) == 0:
% if len(selectable_partitions) == 0:
<div class="is-not-configured has-actions">
<h4 class="title">${_('No content groups exist')}</h4>
<h3 class="title">${_('No visibility settings')}</h3>
<div class="copy">
<p>${_('Use content groups to give groups of students access to a specific set of course content. Create one or more content groups, and make specific components visible to them.')}</p>
<p>${_('No visibility settings are defined for this component, but visibility might be affected by inherited settings.')}</p>
% if settings.FEATURES.get('ENABLE_ENROLLMENT_TRACK_USER_PARTITION'):
<p>${_('You can make this component visible only to specific groups of learners based either on their enrollment track, or by content groups that you create.')}</p>
% else:
<p>${_('You can make this component visible only to specific groups of learners based on content groups that you create.')}</p>
% endif
</div>
<div class="actions">
@@ -31,11 +35,11 @@ is_staff_locked = ancestor_has_staff_lock(xblock)
<span class="icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<p class="copy">
## Translators: Any text between {screen_reader_start} and {screen_reader_end} is only read by screen readers and never shown in the browser.
${_(
"{screen_reader_start}Warning:{screen_reader_end} The Unit this component is contained in is hidden from students. Visibility settings here will be trumped by this."
).format(
screen_reader_start='<span class="sr">',
screen_reader_end='</span>',
${Text(_(
"{screen_reader_start}Warning:{screen_reader_end} The unit that contains this component is hidden from learners. The unit setting overrides the component visibility settings defined here."
)).format(
screen_reader_start=HTML('<span class="sr">'),
screen_reader_end=HTML('</span>'),
)
}
</p>
@@ -43,96 +47,66 @@ is_staff_locked = ancestor_has_staff_lock(xblock)
% endif
</div>
% if len(user_partitions) > 0:
% if len(selectable_partitions) > 0:
<form class="visibility-controls-form" method="post" action="">
<div role="group" aria-labelledby="visibility-title">
<div class="modal-section visibility-controls">
<h3 class="modal-section-title">${_('Make visible to:')}</h3>
<div class="modal-section-content">
<section class="visibility-controls-primary">
<div class="list-fields list-radio">
<div class="field field-radio field-visibility-level">
<input type="radio" id="visibility-level-all" name="visibility-level" value="" class="input input-radio visibility-level-all" ${'checked="checked"' if not has_selected_groups else ''} />
<label for="visibility-level-all" class="label">${_('All Students and Staff')}</label>
</div>
<div class="field field-radio field-visibility-level">
<input type="radio" id="visibility-level-specific" name="visibility-level" value="" class="input input-radio visibility-level-specific" ${'checked="checked"' if has_selected_groups else ''} />
<label for="visibility-level-specific" class="label">${_('Specific Content Groups')}</label>
</div>
</div>
</section>
<div class="wrapper-visibility-specific">
<section class="visibility-controls-secondary">
<div class="visibility-controls-group">
<h4 class="visibility-controls-title modal-subsection-title sr">${_('Content Groups')}</h4>
<div class="list-fields list-checkbox">
% for partition in cohort_partitions:
% for group in partition["groups"]:
<div class="field field-checkbox field-visibility-content-group ${'was-removed' if group["deleted"] else ''}">
<input type="checkbox"
id="visibility-content-group-${partition["id"]}-${group["id"]}"
name="visibility-content-group"
value="${partition["id"]}-${group["id"]}"
class="input input-checkbox"
${'checked="checked"' if group["selected"] else ''}
/>
% if group["deleted"]:
<label for="visibility-content-group-${partition["id"]}-${group["id"]}" class="label">
${_('Deleted Content Group')}
<span class="note">${_('Content group no longer exists. Please choose another or allow access to All Students and staff')}</span>
</label>
% else:
<label for="visibility-content-group-${partition["id"]}-${group["id"]}" class="label">${group["name"] | h}</label>
% endif
</div>
% endfor
% endfor
## Allow only one verification checkpoint to be selected at a time.
% if verification_partitions:
<div role="group" aria-labelledby="verification-access-title">
<div id="verification-access-title" class="sr">${_('Verification Checkpoint')}</div>
<div class="field field-checkbox field-visibility-verification">
<input type="checkbox"
id="verification-access-checkbox"
name="verification-access-checkbox"
class="input input-checkbox"
value=""
aria-describedby="verification-help-text"
${'checked="checked"' if selected_verified_partition_id is not None else ''}
/>
<label for="verification-access-checkbox" class="label">
${_('Verification Checkpoint')}:
</label>
<label class="sr" for="verification-access-dropdown">
${_('Verification checkpoint to complete')}
</label>
<select id="verification-access-dropdown">
% for partition in verification_partitions:
<option
value="${partition["id"]}"
${ "selected" if partition["id"] == selected_verified_partition_id else ""}
>${partition["name"]}</option>
% endfor
</select>
<div class="note" id="verification-help-text">
${_("Learners who require verification must pass the selected checkpoint to see the content in this component. Learners who do not require verification see this content by default.")}
</div>
</div>
</div>
<h3 class="modal-section-title visibility-header" id="visibility-title">
<span class="current-visibility-title">
<span class="icon fa fa-eye" aria-hidden="true"></span>
<span>${_('Currently visible to:')}</span>
</span>
% if selected_partition_index == -1:
<span>${_('All Learners and Staff')}</span>
% else:
<span>${selected_groups_label}</span>
% endif
</h3>
<div class="modal-section-content partition-visibility">
<label class="group-select-title">${_('Change visibility to:')}
<select>
<option value="-1" selected ="selected">
% if selected_partition_index == -1:
${_('Choose one')}
% else:
${_('All Learners and Staff')}
% endif
</div>
</div>
</section>
</option>
% for index, partition in enumerate(selectable_partitions):
<option value="${partition["id"]}" id="visibility-partition-${partition["id"]}" ${'selected="selected"' if selected_partition_index == index else ''}}>
${partition["name"]}
</option>
% endfor
</select>
</label>
% for index, partition in enumerate(selectable_partitions):
<div role="group" aria-labelledby="partition-group-directions-${partition["id"]}" aria-describedby="visibility-partition-${partition["id"]}"
class="partition-group-control partition-group-control-${partition["id"]} ${'is-hidden' if selected_partition_index != index else ''}">
<div class="partition-group-directions" id="partition-group-directions-${partition["id"]}">${_('Select one or more groups:')}
% for group in partition["groups"]:
<div class="field partition-group-visibility partition-group-visibility-${partition["id"]} ${'was-removed' if group["deleted"] else ''}">
<input type="checkbox"
id="visibility-group-${partition["id"]}-${group["id"]}"
name="visibility-group"
value="${group["id"]}"
class="input input-checkbox"
${'checked="checked"' if group["selected"] else ''}
/>
% if group["deleted"]:
<label for="visibility-group-${partition["id"]}-${group["id"]}" class="label">
${_("Deleted Group")}
<span class="note">${_('This group no longer exists. Choose another group or make this component visible to All Learners and Staff.')}</span>
</label>
% else:
<label for="visibility-group-${partition["id"]}-${group["id"]}" class="label">${group["name"]}</label>
% endif
</div>
% endfor
</div>
</div>
% endfor
</div>
</div>
</form>
% endif
% endif