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 ...@@ -8,8 +8,8 @@ from util.db import generate_int_id, MYSQL_MAX_INT
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from contentstore.utils import reverse_usage_url from contentstore.utils import reverse_usage_url
from xmodule.partitions.partitions import UserPartition from xmodule.partitions.partitions import UserPartition, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions_service import get_all_partitions_for_course, 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 xmodule.split_test_module import get_split_user_partitions
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
...@@ -18,11 +18,11 @@ MINIMUM_GROUP_ID = MINIMUM_STATIC_PARTITION_ID ...@@ -18,11 +18,11 @@ MINIMUM_GROUP_ID = MINIMUM_STATIC_PARTITION_ID
RANDOM_SCHEME = "random" RANDOM_SCHEME = "random"
COHORT_SCHEME = "cohort" COHORT_SCHEME = "cohort"
# Note: the following content group configuration strings are not CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
# translated since they are not visible to users. 'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.'
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = 'The groups in this configuration can be mapped to cohort groups in the LMS.' )
CONTENT_GROUP_CONFIGURATION_NAME = 'Content Group Configuration' CONTENT_GROUP_CONFIGURATION_NAME = _('Content Groups')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -510,7 +510,7 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase): ...@@ -510,7 +510,7 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
self.assertEqual(len(groups), 3) self.assertEqual(len(groups), 3)
self.assertEqual(groups[2], { self.assertEqual(groups[2], {
"id": 3, "id": 3,
"name": "Deleted group", "name": "Deleted Group",
"selected": True, "selected": True,
"deleted": True "deleted": True
}) })
......
...@@ -401,7 +401,7 @@ def get_user_partition_info(xblock, schemes=None, course=None): ...@@ -401,7 +401,7 @@ def get_user_partition_info(xblock, schemes=None, course=None):
for gid in missing_group_ids: for gid in missing_group_ids:
groups.append({ groups.append({
"id": gid, "id": gid,
"name": _("Deleted group"), "name": _("Deleted Group"),
"selected": True, "selected": True,
"deleted": True, "deleted": True,
}) })
...@@ -429,30 +429,45 @@ def get_visibility_partition_info(xblock): ...@@ -429,30 +429,45 @@ def get_visibility_partition_info(xblock):
Returns: dict Returns: dict
""" """
user_partitions = get_user_partition_info(xblock, schemes=["verification", "cohort"]) selectable_partitions = []
cohort_partitions = [] # We wish to display enrollment partitions before cohort partitions.
verification_partitions = [] enrollment_user_partitions = get_user_partition_info(xblock, schemes=["enrollment_track"])
has_selected_groups = False
selected_verified_partition_id = None # For enrollment partitions, we only show them if there is a selected group or
# or if the number of groups > 1.
# Pre-process the partitions to make it easier to display the UI for partition in enrollment_user_partitions:
for p in user_partitions: if len(partition["groups"]) > 1 or any(group["selected"] for group in partition["groups"]):
has_selected = any(g["selected"] for g in p["groups"]) selectable_partitions.append(partition)
has_selected_groups = has_selected_groups or has_selected
# Now add the cohort user partitions.
if p["scheme"] == "cohort": selectable_partitions = selectable_partitions + get_user_partition_info(xblock, schemes=["cohort"])
cohort_partitions.append(p)
elif p["scheme"] == "verification": # Find the first partition with a selected group. That will be the one initially enabled in the dialog
verification_partitions.append(p) # (if the course has only been added in Studio, only one partition should have a selected group).
if has_selected: selected_partition_index = -1
selected_verified_partition_id = p["id"]
# 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 { return {
"user_partitions": user_partitions, "selectable_partitions": selectable_partitions,
"cohort_partitions": cohort_partitions, "selected_partition_index": selected_partition_index,
"verification_partitions": verification_partitions, "selected_groups_label": selected_groups_label,
"has_selected_groups": has_selected_groups,
"selected_verified_partition_id": selected_verified_partition_id,
} }
......
...@@ -8,7 +8,7 @@ import ddt ...@@ -8,7 +8,7 @@ import ddt
from mock import patch from mock import patch
from contentstore.utils import reverse_course_url, reverse_usage_url 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 contentstore.tests.utils import CourseTestCase
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -240,7 +240,7 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations ...@@ -240,7 +240,7 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'First name') self.assertContains(response, 'First name')
self.assertContains(response, 'Group C') 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): def test_unsupported_http_accept_header(self):
""" """
......
...@@ -44,8 +44,9 @@ from xblock.exceptions import NoSuchHandlerError ...@@ -44,8 +44,9 @@ from xblock.exceptions import NoSuchHandlerError
from xblock_django.user_service import DjangoXBlockUserService from xblock_django.user_service import DjangoXBlockUserService
from opaque_keys.edx.keys import UsageKey, CourseKey from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import (
from xmodule.partitions.partitions_service import ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID Group, UserPartition, ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID
)
class AsideTest(XBlockAside): class AsideTest(XBlockAside):
...@@ -348,9 +349,9 @@ class GetItemTest(ItemTest): ...@@ -348,9 +349,9 @@ class GetItemTest(ItemTest):
self.course.user_partitions = [ self.course.user_partitions = [
UserPartition( UserPartition(
id=MINIMUM_STATIC_PARTITION_ID, id=MINIMUM_STATIC_PARTITION_ID,
name="Verification user partition", name="Random user partition",
scheme=UserPartition.get_scheme("verification"), scheme=UserPartition.get_scheme("random"),
description="Verification user partition", description="Random user partition",
groups=[ groups=[
Group(id=MINIMUM_STATIC_PARTITION_ID + 1, name="Group A"), # See note above. 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. Group(id=MINIMUM_STATIC_PARTITION_ID + 2, name="Group B"), # See note above.
...@@ -370,7 +371,7 @@ class GetItemTest(ItemTest): ...@@ -370,7 +371,7 @@ class GetItemTest(ItemTest):
self.assertEqual(result["user_partitions"], [ self.assertEqual(result["user_partitions"], [
{ {
"id": ENROLLMENT_TRACK_PARTITION_ID, "id": ENROLLMENT_TRACK_PARTITION_ID,
"name": "Enrollment Track Partition", "name": "Enrollment Tracks",
"scheme": "enrollment_track", "scheme": "enrollment_track",
"groups": [ "groups": [
{ {
...@@ -383,8 +384,8 @@ class GetItemTest(ItemTest): ...@@ -383,8 +384,8 @@ class GetItemTest(ItemTest):
}, },
{ {
"id": MINIMUM_STATIC_PARTITION_ID, "id": MINIMUM_STATIC_PARTITION_ID,
"name": "Verification user partition", "name": "Random user partition",
"scheme": "verification", "scheme": "random",
"groups": [ "groups": [
{ {
"id": MINIMUM_STATIC_PARTITION_ID + 1, "id": MINIMUM_STATIC_PARTITION_ID + 1,
......
...@@ -256,19 +256,6 @@ function(Backbone, _, str, ModuleUtils) { ...@@ -256,19 +256,6 @@ function(Backbone, _, str, ModuleUtils) {
*/ */
isEditableOnCourseOutline: function() { isEditableOnCourseOutline: function() {
return this.isSequential() || this.isChapter() || this.isVertical(); 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; return XBlockInfo;
......
...@@ -15,7 +15,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -15,7 +15,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'use strict'; 'use strict';
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor, var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor, StaffLockEditor, ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor, StaffLockEditor,
ContentVisibilityEditor, VerificationAccessEditor, TimedExaminationPreferenceEditor, AccessEditor; ContentVisibilityEditor, TimedExaminationPreferenceEditor, AccessEditor;
CourseOutlineXBlockModal = BaseModal.extend({ CourseOutlineXBlockModal = BaseModal.extend({
events: _.extend({}, BaseModal.prototype.events, { events: _.extend({}, BaseModal.prototype.events, {
...@@ -720,109 +720,6 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -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 { return {
getModal: function(type, xblockInfo, options) { getModal: function(type, xblockInfo, options) {
if (type === 'edit') { if (type === 'edit') {
...@@ -837,10 +734,6 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -837,10 +734,6 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var editors = []; var editors = [];
if (xblockInfo.isVertical()) { if (xblockInfo.isVertical()) {
editors = [StaffLockEditor]; editors = [StaffLockEditor];
if (xblockInfo.hasVerifiedCheckpoints()) {
editors.push(VerificationAccessEditor);
}
} else { } else {
tabs = [ tabs = [
{ {
......
...@@ -5,25 +5,26 @@ ...@@ -5,25 +5,26 @@
'use strict'; 'use strict';
function VisibilityEditorView(runtime, element) { function VisibilityEditorView(runtime, element) {
this.getGroupAccess = function() { this.getGroupAccess = function() {
var groupAccess = {}, var groupAccess = {},
checkboxValues,
partitionId, partitionId,
groupId, groupId;
// This constant MUST match the group ID // Get the selected user partition (only allowed to select one).
// defined by VerificationPartitionScheme on the backend! partitionId = parseInt(element.find('.partition-visibility select').val(), 10);
ALLOW_GROUP_ID = 1;
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 {}; return {};
} }
// Cohort partitions (user is allowed to select more than one) // Otherwise get the checked groups within the selected partition.
element.find('.field-visibility-content-group input:checked').each(function(index, input) { element.find(
checkboxValues = $(input).val().split('-'); '.partition-group-visibility-' + partitionId + ' input:checked'
partitionId = parseInt(checkboxValues[0], 10); ).each(function(index, input) {
groupId = parseInt(checkboxValues[1], 10); groupId = parseInt($(input).val(), 10);
if (groupAccess.hasOwnProperty(partitionId)) { if (groupAccess.hasOwnProperty(partitionId)) {
groupAccess[partitionId].push(groupId); groupAccess[partitionId].push(groupId);
...@@ -32,38 +33,25 @@ ...@@ -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; return groupAccess;
}; };
// When selecting "all students and staff", uncheck the specific groups element.find('.partition-visibility select').change(function(event) {
element.find('.field-visibility-level input').change(function(event) { var partitionId;
if ($(event.target).hasClass('visibility-level-all')) {
element.find('.field-visibility-content-group input, .field-visibility-verification input') // Hide all the partition group options.
.prop('checked', false); 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() { VisibilityEditorView.prototype.collectFieldData = function collectFieldData() {
return { return {metadata: {group_access: this.getGroupAccess()}};
metadata: {
'group_access': this.getGroupAccess()
}
};
}; };
function initializeVisibilityEditor(runtime, element) { function initializeVisibilityEditor(runtime, element) {
......
...@@ -91,3 +91,5 @@ ...@@ -91,3 +91,5 @@
// CAPA Problem Feedback // CAPA Problem Feedback
@import 'edx-pattern-library-shims/buttons'; @import 'edx-pattern-library-shims/buttons';
@import 'edx-pattern-library-shims/base/variables';
// studio - elements - modal-window // studio - elements - modal-window
// ======================== // ========================
@import 'edx-pattern-library-shims/base/variables';
// start with the view/body // start with the view/body
[class*="view-"] { [class*="view-"] {
...@@ -482,59 +484,59 @@ ...@@ -482,59 +484,59 @@
// MODAL TYPE: component - visibility modal // MODAL TYPE: component - visibility modal
.xblock-visibility_view { .xblock-visibility_view {
.visibility-controls-secondary { // We don't wish the dialog to resize for the common case of 2 groups.
max-height: 100%; min-height: 190px;
overflow-y: auto;
@include margin(($baseline*0.75), 0, 0, $baseline); .visibility-header {
padding-bottom: $baseline;
margin-bottom: 0;
color: $gray-d3;
} }
.visibility-controls-group { .current-visibility-title {
@extend %wipe-last-child; font-weight: font-weight(semi-bold);
margin-bottom: $baseline;
.icon {
@include margin-right($baseline/8);
}
} }
// UI: form fields .group-select-title {
.list-fields { font-weight: font-weight(semi-bold);
font-size: inherit;
}
.field { .partition-visibility {
@extend %wipe-last-child; padding-top: $baseline;
margin-bottom: ($baseline/4); }
label { // UI: form fields
@extend %t-copy-sub1; .partition-group-control {
}
}
// UI: radio and checkbox inputs padding-top: ($baseline/2);
.field-radio, .field-checkbox {
.field {
margin-top: ($baseline/4);
label { label {
@include margin-left($baseline/4); @include margin-left($baseline/4);
font-size: inherit;
} }
} }
} }
.field-visibility-verification { // CASE: content or enrollment group has been removed
.note { .partition-group-visibility.was-removed {
@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 {
.input-checkbox:checked ~ label { .input-checkbox:checked ~ label {
color: $color-error; color: $error-color;
} }
.note { .note {
@extend %t-copy-sub2; @extend %t-copy-sub2;
@extend %t-regular; @extend %t-regular;
display: block; display: block;
color: $color-error; color: $error-color;
} }
} }
...@@ -698,7 +700,7 @@ ...@@ -698,7 +700,7 @@
} }
// UI: staff lock section // 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 { .checkbox-cosmetic .input-checkbox {
@extend %cont-text-sr; @extend %cont-text-sr;
...@@ -730,13 +732,6 @@ ...@@ -730,13 +732,6 @@
} }
} }
.verification-access {
.checkbox-cosmetic .label {
@include float(left);
margin: 2px 6px 0 0;
}
}
// UI: timed and proctored exam section // UI: timed and proctored exam section
.edit-settings-timed-examination { .edit-settings-timed-examination {
......
...@@ -86,7 +86,7 @@ var visibleToStaffOnly = visibilityState === 'staff_only'; ...@@ -86,7 +86,7 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<% if (hasContentGroupComponents) { %> <% if (hasContentGroupComponents) { %>
<p class="note-visibility"> <p class="note-visibility">
<span class="icon fa fa-eye" aria-hidden="true"></span> <span class="icon fa fa-eye" aria-hidden="true"></span>
<span class="note-copy"><%- gettext("Some content in this unit is visible only to particular content groups") %></span> <span class="note-copy"><%- gettext("Some content in this unit is visible only to specific groups of learners.") %></span>
</p> </p>
<% } %> <% } %>
<ul class="actions-inline"> <ul class="actions-inline">
......
...@@ -8,6 +8,16 @@ from stevedore.extension import ExtensionManager ...@@ -8,6 +8,16 @@ from stevedore.extension import ExtensionManager
# pylint: disable=redefined-builtin # 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): class UserPartitionError(Exception):
""" """
Base Exception for when an error was found regarding user partitions. Base Exception for when an error was found regarding user partitions.
......
...@@ -7,23 +7,13 @@ from django.conf import settings ...@@ -7,23 +7,13 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import logging 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 from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__) 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. # settings will not be available when running nosetests.
FEATURES = getattr(settings, 'FEATURES', {}) FEATURES = getattr(settings, 'FEATURES', {})
...@@ -84,7 +74,7 @@ def _create_enrollment_track_partition(course): ...@@ -84,7 +74,7 @@ def _create_enrollment_track_partition(course):
partition = enrollment_track_scheme.create_user_partition( partition = enrollment_track_scheme.create_user_partition(
id=ENROLLMENT_TRACK_PARTITION_ID, id=ENROLLMENT_TRACK_PARTITION_ID,
name=_(u"Enrollment Track Partition"), name=_(u"Enrollment Tracks"),
description=_(u"Partition for segmenting users by enrollment track"), description=_(u"Partition for segmenting users by enrollment track"),
parameters={"course_id": unicode(course.id)} parameters={"course_id": unicode(course.id)}
) )
......
...@@ -9,10 +9,11 @@ from mock import Mock ...@@ -9,10 +9,11 @@ from mock import Mock
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from stevedore.extension import Extension, ExtensionManager from stevedore.extension import Extension, ExtensionManager
from xmodule.partitions.partitions import ( 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 ( 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 ...@@ -13,8 +13,7 @@ from xmodule.tests import get_test_system
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
from xmodule.validation import StudioValidationMessage from xmodule.validation import StudioValidationMessage
from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields, get_split_user_partitions from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields, get_split_user_partitions
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions_service import MINIMUM_STATIC_PARTITION_ID
class SplitTestModuleFactory(xml.XmlImportFactory): class SplitTestModuleFactory(xml.XmlImportFactory):
......
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from common.test.acceptance.pages.common.utils import click_css 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 from selenium.webdriver.support.ui import Select
...@@ -108,43 +109,83 @@ class ComponentVisibilityEditorView(BaseComponentEditorView): ...@@ -108,43 +109,83 @@ class ComponentVisibilityEditorView(BaseComponentEditorView):
""" """
A :class:`.PageObject` representing the rendered view of a component visibility editor. 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 @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 return self.q(css=self._bounded_selector(self.OPTION_SELECTOR)).results
@property @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 = [] results = []
for option in self.all_options: for option in self.all_group_options:
button = option.find_element_by_css_selector('input.input') checkbox = option.find_element_by_css_selector('input')
if button.is_selected(): if checkbox.is_selected():
results.append(option) results.append(option)
return results 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: Arguments:
label_text (str): Text of a label accompanying the input group_name (str): The name of the group.
which should be clicked.
save (boolean): Whether the "save" button should be clicked save (boolean): Whether the "save" button should be clicked
afterwards. afterwards.
Returns: 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: for option in self.all_group_options:
if label_text in option.text: if group_name in option.text:
option.click() checkbox = option.find_element_by_css_selector('input')
checkbox.click()
if save: if save:
self.save() self.save()
return True return True
return False 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): ...@@ -174,22 +174,18 @@ class CoursewareSearchCohortTest(ContainerBase):
""" """
container_page = self.go_to_unit_page() 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. Set visibility on html blocks to specified groups.
""" """
html_block = container_page.xblocks[html_block_index] html_block = container_page.xblocks[html_block_index]
html_block.edit_visibility() html_block.edit_visibility()
if second_content_group: visibility_dialog = ComponentVisibilityEditorView(self.browser, html_block.locator)
ComponentVisibilityEditorView(self.browser, html_block.locator).select_option( visibility_dialog.select_groups_in_partition_scheme(visibility_dialog.CONTENT_GROUP_PARTITION, groups)
second_content_group, save=False
)
ComponentVisibilityEditorView(self.browser, html_block.locator).select_option(content_group)
set_visibility(1, self.content_group_a) set_visibility(1, [self.content_group_a])
set_visibility(2, self.content_group_b) set_visibility(2, [self.content_group_b])
set_visibility(3, self.content_group_a, 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
container_page.publish_action.click() container_page.publish_action.click()
......
...@@ -17,7 +17,6 @@ from common.test.acceptance.pages.lms.courseware import CoursewarePage ...@@ -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.pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage
from common.test.acceptance.tests.lms.test_lms_user_preview import verify_expected_problem_visibility 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 from bok_choy.page_object import XSS_INJECTION
...@@ -121,18 +120,15 @@ class EndToEndCohortedCoursewareTest(ContainerBase): ...@@ -121,18 +120,15 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
""" """
container_page = self.go_to_unit_page() 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 = container_page.xblocks[problem_index]
problem.edit_visibility() problem.edit_visibility()
if second_content_group: visibility_dialog = ComponentVisibilityEditorView(self.browser, problem.locator)
ComponentVisibilityEditorView(self.browser, problem.locator).select_option( visibility_dialog.select_groups_in_partition_scheme(visibility_dialog.CONTENT_GROUP_PARTITION, groups)
second_content_group, save=False
)
ComponentVisibilityEditorView(self.browser, problem.locator).select_option(content_group)
set_visibility(1, self.content_group_a) set_visibility(1, [self.content_group_a])
set_visibility(2, self.content_group_b) set_visibility(2, [self.content_group_b])
set_visibility(3, self.content_group_a, self.content_group_b) set_visibility(3, [self.content_group_a, self.content_group_b])
container_page.publish_action.click() container_page.publish_action.click()
......
...@@ -44,8 +44,7 @@ from xmodule.course_module import ( ...@@ -44,8 +44,7 @@ from xmodule.course_module import (
CATALOG_VISIBILITY_NONE, CATALOG_VISIBILITY_NONE,
) )
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions_service import MINIMUM_STATIC_PARTITION_ID
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
......
...@@ -15,6 +15,9 @@ from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPa ...@@ -15,6 +15,9 @@ from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPa
# more information can be found here: https://openedx.atlassian.net/browse/PLAT-902 # more information can be found here: https://openedx.atlassian.net/browse/PLAT-902
_ = lambda text: text _ = 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): class GroupAccessDict(Dict):
"""Special Dict class for serializing the group_access field""" """Special Dict class for serializing the group_access field"""
...@@ -165,14 +168,14 @@ class LmsBlockMixin(XBlockMixin): ...@@ -165,14 +168,14 @@ class LmsBlockMixin(XBlockMixin):
validation.add( validation.add(
ValidationMessage( ValidationMessage(
ValidationMessage.ERROR, ValidationMessage.ERROR,
_(u"This component refers to deleted or invalid content group configurations.") INVALID_USER_PARTITION_VALIDATION
) )
) )
if has_invalid_groups: if has_invalid_groups:
validation.add( validation.add(
ValidationMessage( ValidationMessage(
ValidationMessage.ERROR, ValidationMessage.ERROR,
_(u"This component refers to deleted or invalid content groups.") INVALID_USER_PARTITION_GROUP_VALIDATION
) )
) )
return validation return validation
...@@ -4,6 +4,7 @@ Tests of the LMS XBlock Mixin ...@@ -4,6 +4,7 @@ Tests of the LMS XBlock Mixin
import ddt import ddt
from nose.plugins.attrib import attr 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 xblock.validation import ValidationMessage
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import CourseFactory, ToyCourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ToyCourseFactory, ItemFactory
...@@ -90,7 +91,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase): ...@@ -90,7 +91,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1) self.assertEqual(len(validation.messages), 1)
self.verify_validation_message( self.verify_validation_message(
validation.messages[0], validation.messages[0],
u"This component refers to deleted or invalid content group configurations.", INVALID_USER_PARTITION_VALIDATION,
ValidationMessage.ERROR, ValidationMessage.ERROR,
) )
...@@ -102,7 +103,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase): ...@@ -102,7 +103,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1) self.assertEqual(len(validation.messages), 1)
self.verify_validation_message( self.verify_validation_message(
validation.messages[0], validation.messages[0],
u"This component refers to deleted or invalid content group configurations.", INVALID_USER_PARTITION_VALIDATION,
ValidationMessage.ERROR, ValidationMessage.ERROR,
) )
...@@ -115,7 +116,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase): ...@@ -115,7 +116,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1) self.assertEqual(len(validation.messages), 1)
self.verify_validation_message( self.verify_validation_message(
validation.messages[0], validation.messages[0],
u"This component refers to deleted or invalid content groups.", INVALID_USER_PARTITION_GROUP_VALIDATION,
ValidationMessage.ERROR, ValidationMessage.ERROR,
) )
...@@ -125,7 +126,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase): ...@@ -125,7 +126,7 @@ class XBlockValidationTest(LmsXBlockMixinTestCase):
self.assertEqual(len(validation.messages), 1) self.assertEqual(len(validation.messages), 1)
self.verify_validation_message( self.verify_validation_message(
validation.messages[0], validation.messages[0],
u"This component refers to deleted or invalid content groups.", INVALID_USER_PARTITION_GROUP_VALIDATION,
ValidationMessage.ERROR, ValidationMessage.ERROR,
) )
......
...@@ -12,8 +12,7 @@ from student.models import CourseEnrollment ...@@ -12,8 +12,7 @@ from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.partitions.partitions import UserPartition from xmodule.partitions.partitions import UserPartition, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions_service import MINIMUM_STATIC_PARTITION_ID
class EnrollmentTrackUserPartitionTest(SharedModuleStoreTestCase): class EnrollmentTrackUserPartitionTest(SharedModuleStoreTestCase):
...@@ -160,7 +159,7 @@ def create_enrollment_track_partition(course): ...@@ -160,7 +159,7 @@ def create_enrollment_track_partition(course):
enrollment_track_scheme = UserPartition.get_scheme("enrollment_track") enrollment_track_scheme = UserPartition.get_scheme("enrollment_track")
partition = enrollment_track_scheme.create_user_partition( partition = enrollment_track_scheme.create_user_partition(
id=1, id=1,
name="TestEnrollment Track Partition", name="Test Enrollment Track Partition",
description="Test partition for segmenting users by enrollment track", description="Test partition for segmenting users by enrollment track",
parameters={"course_id": unicode(course.id)} 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