Commit dbae1e39 by cahrens

Add ability to set visibility by enrollment track.

TNL-6744
parent 073826ca
......@@ -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__)
......
......@@ -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
})
......
......@@ -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
# 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
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"]
selectable_partitions = []
# We wish to display enrollment partitions before cohort partitions.
enrollment_user_partitions = get_user_partition_info(xblock, schemes=["enrollment_track"])
# 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)
# 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,
}
......
......@@ -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):
"""
......
......@@ -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,
......
"""
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]
)
......@@ -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;
......
......@@ -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 = [
{
......
......@@ -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) {
......
......@@ -91,3 +91,5 @@
// CAPA Problem Feedback
@import 'edx-pattern-library-shims/buttons';
@import 'edx-pattern-library-shims/base/variables';
// 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);
}
}
// UI: form fields
.list-fields {
.group-select-title {
font-weight: font-weight(semi-bold);
font-size: inherit;
}
.field {
@extend %wipe-last-child;
margin-bottom: ($baseline/4);
.partition-visibility {
padding-top: $baseline;
}
label {
@extend %t-copy-sub1;
}
}
// UI: form fields
.partition-group-control {
// UI: radio and checkbox inputs
.field-radio, .field-checkbox {
padding-top: ($baseline/2);
.field {
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 {
......
......@@ -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">
......
<%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>
<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
</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>
<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>
% 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>
</div>
</section>
</div>
% endfor
</div>
</div>
% endfor
</div>
</div>
</form>
% endif
% endif
\ No newline at end of file
......@@ -8,6 +8,16 @@ from stevedore.extension import ExtensionManager
# pylint: disable=redefined-builtin
# UserPartition IDs must be unique. The Cohort and Random UserPartitions (when they are
# created via Studio) choose an unused ID in the range of 100 (historical) to MAX_INT. Therefore the
# dynamic UserPartitionIDs must be under 100, and they have to be hard-coded to ensure
# they are always the same whenever the dynamic partition is added (since the UserPartition
# ID is stored in the xblock group_access dict).
ENROLLMENT_TRACK_PARTITION_ID = 50
MINIMUM_STATIC_PARTITION_ID = 100
class UserPartitionError(Exception):
"""
Base Exception for when an error was found regarding user partitions.
......
......@@ -7,23 +7,13 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
import logging
from xmodule.partitions.partitions import UserPartition, UserPartitionError
from xmodule.partitions.partitions import UserPartition, UserPartitionError, ENROLLMENT_TRACK_PARTITION_ID
from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__)
# UserPartition IDs must be unique. The Cohort and Random UserPartitions (when they are
# created via Studio) choose an unused ID in the range of 100 (historical) to MAX_INT. Therefore the
# dynamic UserPartitionIDs must be under 100, and they have to be hard-coded to ensure
# they are always the same whenever the dynamic partition is added (since the UserPartition
# ID is stored in the xblock group_access dict).
ENROLLMENT_TRACK_PARTITION_ID = 50
MINIMUM_STATIC_PARTITION_ID = 100
# settings will not be available when running nosetests.
FEATURES = getattr(settings, 'FEATURES', {})
......@@ -84,7 +74,7 @@ def _create_enrollment_track_partition(course):
partition = enrollment_track_scheme.create_user_partition(
id=ENROLLMENT_TRACK_PARTITION_ID,
name=_(u"Enrollment Track Partition"),
name=_(u"Enrollment Tracks"),
description=_(u"Partition for segmenting users by enrollment track"),
parameters={"course_id": unicode(course.id)}
)
......
......@@ -9,10 +9,11 @@ from mock import Mock
from opaque_keys.edx.locator import CourseLocator
from stevedore.extension import Extension, ExtensionManager
from xmodule.partitions.partitions import (
Group, UserPartition, UserPartitionError, NoSuchUserPartitionGroupError, USER_PARTITION_SCHEME_NAMESPACE
Group, UserPartition, UserPartitionError, NoSuchUserPartitionGroupError,
USER_PARTITION_SCHEME_NAMESPACE, ENROLLMENT_TRACK_PARTITION_ID
)
from xmodule.partitions.partitions_service import (
PartitionService, get_all_partitions_for_course, ENROLLMENT_TRACK_PARTITION_ID, FEATURES
PartitionService, get_all_partitions_for_course, FEATURES
)
......
......@@ -13,8 +13,7 @@ from xmodule.tests import get_test_system
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
from xmodule.validation import StudioValidationMessage
from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields, get_split_user_partitions
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.partitions_service import MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions import Group, UserPartition, MINIMUM_STATIC_PARTITION_ID
class SplitTestModuleFactory(xml.XmlImportFactory):
......
from bok_choy.page_object import PageObject
from selenium.webdriver.common.keys import Keys
from common.test.acceptance.pages.common.utils import click_css
from common.test.acceptance.tests.helpers import select_option_by_text, get_selected_option_text
from selenium.webdriver.support.ui import Select
......@@ -108,43 +109,83 @@ class ComponentVisibilityEditorView(BaseComponentEditorView):
"""
A :class:`.PageObject` representing the rendered view of a component visibility editor.
"""
OPTION_SELECTOR = '.modal-section-content .field'
OPTION_SELECTOR = '.partition-group-control .field'
ALL_LEARNERS_AND_STAFF = 'All Learners and Staff'
CONTENT_GROUP_PARTITION = 'Content Groups'
ENROLLMENT_TRACK_PARTITION = "Enrollment Tracks"
@property
def all_options(self):
def all_group_options(self):
"""
Return all visibility options.
Return all partition groups.
"""
return self.q(css=self._bounded_selector(self.OPTION_SELECTOR)).results
@property
def selected_options(self):
def current_groups_message(self):
"""
Return all selected visibility options.
This returns the message shown at the top of the visibility dialog about the
current visibility state (at the time that the dialog was opened).
For example, "Current visible to: All Learners and Staff".
"""
return self.q(css=self._bounded_selector('.visibility-header'))[0].text
@property
def selected_partition_scheme(self):
"""
Return the selected partition scheme (or "All Learners and Staff"
if no partitioning is selected).
"""
selector = self.q(css=self._bounded_selector('.partition-visibility select'))
return get_selected_option_text(selector)
def select_partition_scheme(self, partition_name):
"""
Sets the selected partition scheme to the one with the
matching name.
"""
selector = self.q(css=self._bounded_selector('.partition-visibility select'))
select_option_by_text(selector, partition_name, focus_out=True)
@property
def selected_groups(self):
"""
Return all selected partition groups. If none are selected,
returns an empty array.
"""
results = []
for option in self.all_options:
button = option.find_element_by_css_selector('input.input')
if button.is_selected():
for option in self.all_group_options:
checkbox = option.find_element_by_css_selector('input')
if checkbox.is_selected():
results.append(option)
return results
def select_option(self, label_text, save=True):
def select_group(self, group_name, save=True):
"""
Click the first option which has a label matching `label_text`.
Select the first group which has a label matching `group_name`.
Arguments:
label_text (str): Text of a label accompanying the input
which should be clicked.
group_name (str): The name of the group.
save (boolean): Whether the "save" button should be clicked
afterwards.
Returns:
bool: Whether the label was found and clicked.
bool: Whether a group with the provided name was found and clicked.
"""
for option in self.all_options:
if label_text in option.text:
option.click()
for option in self.all_group_options:
if group_name in option.text:
checkbox = option.find_element_by_css_selector('input')
checkbox.click()
if save:
self.save()
return True
return False
def select_groups_in_partition_scheme(self, partition_name, group_names):
"""
Select groups in the provided partition scheme. The "save"
button is clicked afterwards.
"""
self.select_partition_scheme(partition_name)
for label in group_names:
self.select_group(label, save=False)
self.save()
......@@ -174,22 +174,18 @@ class CoursewareSearchCohortTest(ContainerBase):
"""
container_page = self.go_to_unit_page()
def set_visibility(html_block_index, content_group, second_content_group=None):
def set_visibility(html_block_index, groups):
"""
Set visibility on html blocks to specified groups.
"""
html_block = container_page.xblocks[html_block_index]
html_block.edit_visibility()
if second_content_group:
ComponentVisibilityEditorView(self.browser, html_block.locator).select_option(
second_content_group, save=False
)
ComponentVisibilityEditorView(self.browser, html_block.locator).select_option(content_group)
visibility_dialog = ComponentVisibilityEditorView(self.browser, html_block.locator)
visibility_dialog.select_groups_in_partition_scheme(visibility_dialog.CONTENT_GROUP_PARTITION, groups)
set_visibility(1, self.content_group_a)
set_visibility(2, self.content_group_b)
set_visibility(3, self.content_group_a, self.content_group_b)
set_visibility(4, 'All Students and Staff') # Does not work without this
set_visibility(1, [self.content_group_a])
set_visibility(2, [self.content_group_b])
set_visibility(3, [self.content_group_a, self.content_group_b])
container_page.publish_action.click()
......
......@@ -3,10 +3,15 @@ Acceptance tests for Studio related to the container page.
The container page is used both for displaying units, and
for displaying containers within units.
"""
import datetime
import ddt
from nose.plugins.attrib import attr
from unittest import skip
from base_studio_test import ContainerBase
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
from common.test.acceptance.pages.studio.component_editor import ComponentEditorView, ComponentVisibilityEditorView
from common.test.acceptance.pages.studio.container import ContainerPage
from common.test.acceptance.pages.studio.html_component_editor import HtmlComponentEditorView
......@@ -16,10 +21,9 @@ from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage
from common.test.acceptance.tests.helpers import create_user_partition_json
import datetime
import ddt
from base_studio_test import ContainerBase
from xmodule.partitions.partitions import Group
from xmodule.partitions.partitions import (
Group, ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID
)
class NestedVerticalTest(ContainerBase):
......@@ -313,31 +317,32 @@ class EditContainerTest(NestedVerticalTest):
self.assertEqual(component.student_content, "modified content")
@attr(shard=3)
class EditVisibilityModalTest(ContainerBase):
"""
Tests of the visibility settings modal for components on the unit
page.
"""
VISIBILITY_LABEL_ALL = 'All Students and Staff'
VISIBILITY_LABEL_SPECIFIC = 'Specific Content Groups'
MISSING_GROUP_LABEL = 'Deleted Content Group\nContent group no longer exists. Please choose another or allow access to All Students and staff'
class BaseGroupConfigurationsTest(ContainerBase):
ALL_LEARNERS_AND_STAFF = ComponentVisibilityEditorView.ALL_LEARNERS_AND_STAFF
CHOOSE_ONE = "Choose one"
CONTENT_GROUP_PARTITION = ComponentVisibilityEditorView.CONTENT_GROUP_PARTITION
ENROLLMENT_TRACK_PARTITION = ComponentVisibilityEditorView.ENROLLMENT_TRACK_PARTITION
MISSING_GROUP_LABEL = 'Deleted Group\nThis group no longer exists. Choose another group or make this component visible to All Learners and Staff.'
VALIDATION_ERROR_LABEL = 'This component has validation issues.'
VALIDATION_ERROR_MESSAGE = 'Error:\nThis component refers to deleted or invalid content groups.'
GROUP_VISIBILITY_MESSAGE = 'Some content in this unit is visible only to particular content groups'
VALIDATION_ERROR_MESSAGE = "Error:\nThis component's visibility settings refer to deleted or invalid groups."
GROUP_VISIBILITY_MESSAGE = 'Some content in this unit is visible only to specific groups of learners.'
def setUp(self):
super(EditVisibilityModalTest, self).setUp()
super(BaseGroupConfigurationsTest, self).setUp()
# Set up a cohort-schemed user partition
self.id_base = MINIMUM_STATIC_PARTITION_ID
self.course_fixture._update_xblock(self.course_fixture._course_location, {
"metadata": {
u"user_partitions": [
create_user_partition_json(
0,
'Configuration Dogs, Cats',
self.id_base,
self.CONTENT_GROUP_PARTITION,
'Content Group Partition',
[Group("0", 'Dogs'), Group("1", 'Cats')],
[
Group(self.id_base + 1, 'Dogs'),
Group(self.id_base + 2, 'Cats')
],
scheme="cohort"
)
],
......@@ -368,37 +373,44 @@ class EditVisibilityModalTest(ContainerBase):
component.edit_visibility()
return ComponentVisibilityEditorView(self.browser, component.locator)
def verify_selected_labels(self, visibility_editor, expected_labels):
def verify_current_groups_message(self, visibility_editor, expected_current_groups):
"""
Verify that a visibility editor's selected labels match the
expected ones.
Check that the current visibility is displayed at the top of the dialog.
"""
# If anything other than 'All Students and Staff', is selected,
# 'Specific Content Groups' should be selected as well.
if expected_labels != [self.VISIBILITY_LABEL_ALL]:
expected_labels.append(self.VISIBILITY_LABEL_SPECIFIC)
self.assertItemsEqual(expected_labels, [option.text for option in visibility_editor.selected_options])
self.assertEqual(
"Currently visible to: {groups}".format(groups=expected_current_groups),
visibility_editor.current_groups_message
)
def select_and_verify_saved(self, component, labels, expected_labels=None):
def verify_selected_partition_scheme(self, visibility_editor, expected_scheme):
"""
Check that the expected partition scheme is selected.
"""
self.assertItemsEqual(expected_scheme, visibility_editor.selected_partition_scheme)
def verify_selected_groups(self, visibility_editor, expected_groups):
"""
Check the expected partition groups.
"""
self.assertItemsEqual(expected_groups, [group.text for group in visibility_editor.selected_groups])
def select_and_verify_saved(self, component, partition_label, groups=[]):
"""
Edit the visibility of an xblock on the container page and
verify that the edit persists. If provided, verify that
`expected_labels` are selected after save, otherwise expect
that `labels` are selected after save. Note that `labels`
verify that the edit persists. Note that `groups`
are labels which should be clicked, but not necessarily checked.
"""
if expected_labels is None:
expected_labels = labels
# Make initial edit(s) and save
visibility_editor = self.edit_component_visibility(component)
for label in labels:
visibility_editor.select_option(label, save=False)
visibility_editor.save()
visibility_editor.select_groups_in_partition_scheme(partition_label, groups)
# Re-open the modal and inspect its selected inputs
# Re-open the modal and inspect its selected inputs. If no groups were selected,
# "All Learners" should be selected partitions scheme, and we show "Choose one" in the select.
if not groups:
partition_label = self.CHOOSE_ONE
visibility_editor = self.edit_component_visibility(component)
self.verify_selected_labels(visibility_editor, expected_labels)
self.verify_selected_partition_scheme(visibility_editor, partition_label)
self.verify_selected_groups(visibility_editor, groups)
visibility_editor.save()
def verify_component_validation_error(self, component):
......@@ -436,15 +448,22 @@ class EditVisibilityModalTest(ContainerBase):
verify that there are no missing group messages in the modal
and that there is no validation error on the component.
"""
for option in visibility_editor.selected_options:
for option in visibility_editor.all_group_options:
if option.text == self.MISSING_GROUP_LABEL:
option.click()
visibility_editor.save()
visibility_editor = self.edit_component_visibility(component)
self.assertNotIn(self.MISSING_GROUP_LABEL, [item.text for item in visibility_editor.all_options])
self.assertNotIn(self.MISSING_GROUP_LABEL, [item.text for item in visibility_editor.all_group_options])
visibility_editor.cancel()
self.assertFalse(component.has_validation_error)
@attr(shard=3)
class ContentGroupVisibilityModalTest(BaseGroupConfigurationsTest):
"""
Tests of the visibility settings modal for components on the unit
page (content groups).
"""
def test_default_selection(self):
"""
Scenario: The component visibility modal selects visible to all by default.
......@@ -454,7 +473,10 @@ class EditVisibilityModalTest(ContainerBase):
Then the default visibility selection should be 'All Students and Staff'
And the container page should not display the content visibility warning
"""
self.verify_selected_labels(self.edit_component_visibility(self.html_component), [self.VISIBILITY_LABEL_ALL])
visibility_dialog = self.edit_component_visibility(self.html_component)
self.verify_current_groups_message(visibility_dialog, self.ALL_LEARNERS_AND_STAFF)
self.verify_selected_partition_scheme(visibility_dialog, self.CHOOSE_ONE)
visibility_dialog.cancel()
self.verify_visibility_set(self.html_component, False)
def test_reset_to_all_students_and_staff(self):
......@@ -472,9 +494,9 @@ class EditVisibilityModalTest(ContainerBase):
Then the visibility selection should be 'All Students and Staff'
And the container page should not display the content visibility warning
"""
self.select_and_verify_saved(self.html_component, ['Dogs'])
self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs'])
self.verify_visibility_set(self.html_component, True)
self.select_and_verify_saved(self.html_component, [self.VISIBILITY_LABEL_ALL])
self.select_and_verify_saved(self.html_component, self.ALL_LEARNERS_AND_STAFF)
self.verify_visibility_set(self.html_component, False)
def test_select_single_content_group(self):
......@@ -488,7 +510,7 @@ class EditVisibilityModalTest(ContainerBase):
Then the visibility selection should be 'Dogs' and 'Specific Content Groups'
And the container page should display the content visibility warning
"""
self.select_and_verify_saved(self.html_component, ['Dogs'])
self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs'])
self.verify_visibility_set(self.html_component, True)
def test_select_multiple_content_groups(self):
......@@ -502,7 +524,7 @@ class EditVisibilityModalTest(ContainerBase):
Then the visibility selection should be 'Dogs', 'Cats', and 'Specific Content Groups'
And the container page should display the content visibility warning
"""
self.select_and_verify_saved(self.html_component, ['Dogs', 'Cats'])
self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs', 'Cats'])
self.verify_visibility_set(self.html_component, True)
def test_select_zero_content_groups(self):
......@@ -518,7 +540,7 @@ class EditVisibilityModalTest(ContainerBase):
And the container page should not display the content visibility warning
"""
self.select_and_verify_saved(
self.html_component, [self.VISIBILITY_LABEL_SPECIFIC], expected_labels=[self.VISIBILITY_LABEL_ALL]
self.html_component, self.CONTENT_GROUP_PARTITION
)
self.verify_visibility_set(self.html_component, False)
......@@ -539,11 +561,14 @@ class EditVisibilityModalTest(ContainerBase):
And I should not see any validation errors on the component
And the container page should not display the content visibility warning
"""
self.update_component(self.html_component, {'group_access': {0: [2, 3]}})
self.verify_component_validation_error(self.html_component)
visibility_editor = self.edit_component_visibility(self.html_component)
self.verify_selected_labels(visibility_editor, [self.MISSING_GROUP_LABEL] * 2)
self.remove_missing_groups(visibility_editor, self.html_component)
self.update_component(
self.html_component,
{'group_access': {self.id_base: [self.id_base + 3, self.id_base + 4]}}
)
self._verify_and_remove_missing_content_groups(
"Deleted Group, Deleted Group",
[self.MISSING_GROUP_LABEL] * 2
)
self.verify_visibility_set(self.html_component, False)
def test_found_and_missing_groups(self):
......@@ -563,14 +588,83 @@ class EditVisibilityModalTest(ContainerBase):
And I should not see any validation errors on the component
And the container page should display the content visibility warning
"""
self.update_component(self.html_component, {'group_access': {0: [0, 1, 2, 3]}})
self.update_component(
self.html_component,
{'group_access': {self.id_base: [self.id_base + 1, self.id_base + 2, self.id_base + 3, self.id_base + 4]}}
)
self._verify_and_remove_missing_content_groups(
'Dogs, Cats, Deleted Group, Deleted Group',
['Dogs', 'Cats'] + [self.MISSING_GROUP_LABEL] * 2
)
visibility_editor = self.edit_component_visibility(self.html_component)
self.verify_selected_partition_scheme(visibility_editor, self.CONTENT_GROUP_PARTITION)
expected_groups = ['Dogs', 'Cats']
self.verify_current_groups_message(visibility_editor, ", ".join(expected_groups))
self.verify_selected_groups(visibility_editor, expected_groups)
self.verify_visibility_set(self.html_component, True)
def _verify_and_remove_missing_content_groups(self, current_groups_message, all_group_labels):
self.verify_component_validation_error(self.html_component)
visibility_editor = self.edit_component_visibility(self.html_component)
self.verify_selected_labels(visibility_editor, ['Dogs', 'Cats'] + [self.MISSING_GROUP_LABEL] * 2)
self.verify_selected_partition_scheme(visibility_editor, self.CONTENT_GROUP_PARTITION)
self.verify_current_groups_message(visibility_editor, current_groups_message)
self.verify_selected_groups(visibility_editor, all_group_labels)
self.remove_missing_groups(visibility_editor, self.html_component)
@attr(shard=3)
class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest):
"""
Tests of the visibility settings modal for components on the unit
page (enrollment tracks).
"""
AUDIT_TRACK = "Audit Track"
VERIFIED_TRACK = "Verified Track"
def setUp(self):
super(EnrollmentTrackVisibilityModalTest, self).setUp()
# Add an audit mode to the course
ModeCreationPage(self.browser, self.course_id, mode_slug=u'audit', mode_display_name=self.AUDIT_TRACK).visit()
# Add a verified mode to the course
ModeCreationPage(
self.browser, self.course_id, mode_slug=u'verified',
mode_display_name=self.VERIFIED_TRACK, min_price=10
).visit()
self.container_page = self.go_to_unit_page()
self.html_component = self.container_page.xblocks[1]
# Initially set visibility to Verified track.
self.update_component(
self.html_component,
{'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [2]}} # "2" is Verified
)
def test_setting_enrollment_tracks(self):
"""
Test that enrollment track groups can be selected.
"""
# Open dialog with "Verified" already selected.
visibility_editor = self.edit_component_visibility(self.html_component)
self.verify_selected_labels(visibility_editor, ['Dogs', 'Cats'])
self.verify_visibility_set(self.html_component, True)
self.verify_current_groups_message(visibility_editor, self.VERIFIED_TRACK)
self.verify_selected_partition_scheme(
visibility_editor,
self.ENROLLMENT_TRACK_PARTITION
)
self.verify_selected_groups(visibility_editor, [self.VERIFIED_TRACK])
visibility_editor.cancel()
# 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)
# 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])
@attr(shard=1)
......
......@@ -17,7 +17,6 @@ from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage
from common.test.acceptance.tests.lms.test_lms_user_preview import verify_expected_problem_visibility
from bok_choy.promise import EmptyPromise
from bok_choy.page_object import XSS_INJECTION
......@@ -121,18 +120,15 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
"""
container_page = self.go_to_unit_page()
def set_visibility(problem_index, content_group, second_content_group=None):
def set_visibility(problem_index, groups):
problem = container_page.xblocks[problem_index]
problem.edit_visibility()
if second_content_group:
ComponentVisibilityEditorView(self.browser, problem.locator).select_option(
second_content_group, save=False
)
ComponentVisibilityEditorView(self.browser, problem.locator).select_option(content_group)
visibility_dialog = ComponentVisibilityEditorView(self.browser, problem.locator)
visibility_dialog.select_groups_in_partition_scheme(visibility_dialog.CONTENT_GROUP_PARTITION, groups)
set_visibility(1, self.content_group_a)
set_visibility(2, self.content_group_b)
set_visibility(3, self.content_group_a, self.content_group_b)
set_visibility(1, [self.content_group_a])
set_visibility(2, [self.content_group_b])
set_visibility(3, [self.content_group_a, self.content_group_b])
container_page.publish_action.click()
......
......@@ -44,8 +44,7 @@ from xmodule.course_module import (
CATALOG_VISIBILITY_NONE,
)
from xmodule.error_module import ErrorDescriptor
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.partitions_service import MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions import Group, UserPartition, MINIMUM_STATIC_PARTITION_ID
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
......
......@@ -15,6 +15,9 @@ from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPa
# more information can be found here: https://openedx.atlassian.net/browse/PLAT-902
_ = lambda text: text
INVALID_USER_PARTITION_VALIDATION = _(u"This component's visibility settings refer to deleted or invalid group configurations.")
INVALID_USER_PARTITION_GROUP_VALIDATION = _(u"This component's visibility settings refer to deleted or invalid groups.")
class GroupAccessDict(Dict):
"""Special Dict class for serializing the group_access field"""
......@@ -165,14 +168,14 @@ class LmsBlockMixin(XBlockMixin):
validation.add(
ValidationMessage(
ValidationMessage.ERROR,
_(u"This component refers to deleted or invalid content group configurations.")
INVALID_USER_PARTITION_VALIDATION
)
)
if has_invalid_groups:
validation.add(
ValidationMessage(
ValidationMessage.ERROR,
_(u"This component refers to deleted or invalid content groups.")
INVALID_USER_PARTITION_GROUP_VALIDATION
)
)
return validation
......@@ -4,6 +4,7 @@ Tests of the LMS XBlock Mixin
import ddt
from nose.plugins.attrib import attr
from lms_xblock.mixin import INVALID_USER_PARTITION_VALIDATION, INVALID_USER_PARTITION_GROUP_VALIDATION
from xblock.validation import ValidationMessage
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import CourseFactory, ToyCourseFactory, ItemFactory
......@@ -90,7 +91,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1)
self.verify_validation_message(
validation.messages[0],
u"This component refers to deleted or invalid content group configurations.",
INVALID_USER_PARTITION_VALIDATION,
ValidationMessage.ERROR,
)
......@@ -102,7 +103,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1)
self.verify_validation_message(
validation.messages[0],
u"This component refers to deleted or invalid content group configurations.",
INVALID_USER_PARTITION_VALIDATION,
ValidationMessage.ERROR,
)
......@@ -115,7 +116,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1)
self.verify_validation_message(
validation.messages[0],
u"This component refers to deleted or invalid content groups.",
INVALID_USER_PARTITION_GROUP_VALIDATION,
ValidationMessage.ERROR,
)
......@@ -125,7 +126,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1)
self.verify_validation_message(
validation.messages[0],
u"This component refers to deleted or invalid content groups.",
INVALID_USER_PARTITION_GROUP_VALIDATION,
ValidationMessage.ERROR,
)
......
......@@ -12,8 +12,7 @@ from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.partitions.partitions import UserPartition
from xmodule.partitions.partitions_service import MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions import UserPartition, MINIMUM_STATIC_PARTITION_ID
class EnrollmentTrackUserPartitionTest(SharedModuleStoreTestCase):
......@@ -160,7 +159,7 @@ def create_enrollment_track_partition(course):
enrollment_track_scheme = UserPartition.get_scheme("enrollment_track")
partition = enrollment_track_scheme.create_user_partition(
id=1,
name="TestEnrollment Track Partition",
name="Test Enrollment Track Partition",
description="Test partition for segmenting users by enrollment track",
parameters={"course_id": unicode(course.id)}
)
......
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