Add ability to set visibility by enrollment track.
TNL-6744
This commit is contained in:
@@ -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
|
||||
selectable_partitions = []
|
||||
# We wish to display enrollment partitions before cohort partitions.
|
||||
enrollment_user_partitions = get_user_partition_info(xblock, schemes=["enrollment_track"])
|
||||
|
||||
# Pre-process the partitions to make it easier to display the UI
|
||||
for p in user_partitions:
|
||||
has_selected = any(g["selected"] for g in p["groups"])
|
||||
has_selected_groups = has_selected_groups or has_selected
|
||||
# For enrollment partitions, we only show them if there is a selected group or
|
||||
# or if the number of groups > 1.
|
||||
for partition in enrollment_user_partitions:
|
||||
if len(partition["groups"]) > 1 or any(group["selected"] for group in partition["groups"]):
|
||||
selectable_partitions.append(partition)
|
||||
|
||||
if p["scheme"] == "cohort":
|
||||
cohort_partitions.append(p)
|
||||
elif p["scheme"] == "verification":
|
||||
verification_partitions.append(p)
|
||||
if has_selected:
|
||||
selected_verified_partition_id = p["id"]
|
||||
# Now add the cohort user partitions.
|
||||
selectable_partitions = selectable_partitions + get_user_partition_info(xblock, schemes=["cohort"])
|
||||
|
||||
# Find the first partition with a selected group. That will be the one initially enabled in the dialog
|
||||
# (if the course has only been added in Studio, only one partition should have a selected group).
|
||||
selected_partition_index = -1
|
||||
|
||||
# At the same time, build up all the selected groups as they are displayed in the dialog title.
|
||||
selected_groups_label = ''
|
||||
|
||||
for index, partition in enumerate(selectable_partitions):
|
||||
for group in partition["groups"]:
|
||||
if group["selected"]:
|
||||
if len(selected_groups_label) == 0:
|
||||
selected_groups_label = group['name']
|
||||
else:
|
||||
# Translators: This is building up a list of groups. It is marked for translation because of the
|
||||
# comma, which is used as a separator between each group.
|
||||
selected_groups_label = _('{previous_groups}, {current_group}').format(
|
||||
previous_groups=selected_groups_label,
|
||||
current_group=group['name']
|
||||
)
|
||||
if selected_partition_index == -1:
|
||||
selected_partition_index = index
|
||||
|
||||
return {
|
||||
"user_partitions": user_partitions,
|
||||
"cohort_partitions": cohort_partitions,
|
||||
"verification_partitions": verification_partitions,
|
||||
"has_selected_groups": has_selected_groups,
|
||||
"selected_verified_partition_id": selected_verified_partition_id,
|
||||
"selectable_partitions": selectable_partitions,
|
||||
"selected_partition_index": selected_partition_index,
|
||||
"selected_groups_label": selected_groups_label,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
"""
|
||||
Tests for the Studio authoring XBlock mixin.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
from xmodule.partitions.partitions import (
|
||||
Group, UserPartition, ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID
|
||||
)
|
||||
|
||||
|
||||
class AuthoringMixinTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests the studio authoring XBlock mixin.
|
||||
"""
|
||||
GROUP_NO_LONGER_EXISTS = "This group no longer exists"
|
||||
NO_CONTENT_OR_ENROLLMENT_GROUPS = "No visibility settings are defined for this component"
|
||||
NO_CONTENT_ENROLLMENT_TRACK_ENABLED = "specific groups of learners based either on their enrollment track, or by content groups that you create"
|
||||
NO_CONTENT_ENROLLMENT_TRACK_DISABLED = "specific groups of learners based on content groups that you create"
|
||||
CONTENT_GROUPS_TITLE = "Content Groups"
|
||||
ENROLLMENT_GROUPS_TITLE = "Enrollment Tracks"
|
||||
STAFF_LOCKED = 'The unit that contains this component is hidden from learners'
|
||||
|
||||
FEATURES_WITH_ENROLLMENT_TRACK_DISABLED = settings.FEATURES.copy()
|
||||
FEATURES_WITH_ENROLLMENT_TRACK_DISABLED['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = False
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a simple course with a video component.
|
||||
@@ -47,8 +64,8 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.content_partition = UserPartition(
|
||||
1,
|
||||
'Content Groups',
|
||||
MINIMUM_STATIC_PARTITION_ID,
|
||||
self.CONTENT_GROUPS_TITLE,
|
||||
'Contains Groups for Cohorted Courseware',
|
||||
content_groups,
|
||||
scheme_id='cohort'
|
||||
@@ -56,39 +73,19 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
|
||||
self.course.user_partitions = [self.content_partition]
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
def create_verification_user_partitions(self, checkpoint_names):
|
||||
"""
|
||||
Create user partitions for verification checkpoints.
|
||||
"""
|
||||
scheme = UserPartition.get_scheme("verification")
|
||||
self.course.user_partitions = [
|
||||
UserPartition(
|
||||
id=0,
|
||||
name=checkpoint_name,
|
||||
description="Verification checkpoint",
|
||||
scheme=scheme,
|
||||
groups=[
|
||||
Group(scheme.ALLOW, "Completed verification at {}".format(checkpoint_name)),
|
||||
Group(scheme.DENY, "Did not complete verification at {}".format(checkpoint_name)),
|
||||
],
|
||||
)
|
||||
for checkpoint_name in checkpoint_names
|
||||
]
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
def set_staff_only(self, item_location):
|
||||
"""Make an item visible to staff only."""
|
||||
item = self.store.get_item(item_location)
|
||||
item.visible_to_staff_only = True
|
||||
self.store.update_item(item, self.user.id)
|
||||
|
||||
def set_group_access(self, item_location, group_ids):
|
||||
def set_group_access(self, item_location, group_ids, partition_id=None):
|
||||
"""
|
||||
Set group_access for the specified item to the specified group
|
||||
ids within the content partition.
|
||||
"""
|
||||
item = self.store.get_item(item_location)
|
||||
item.group_access[self.content_partition.id] = group_ids
|
||||
item.group_access[self.content_partition.id if partition_id is None else partition_id] = group_ids
|
||||
self.store.update_item(item, self.user.id)
|
||||
|
||||
def verify_visibility_view_contains(self, item_location, substrings):
|
||||
@@ -101,39 +98,70 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
|
||||
for string in substrings:
|
||||
self.assertIn(string, html)
|
||||
|
||||
def verify_visibility_view_does_not_contain(self, item_location, substrings):
|
||||
"""
|
||||
Verify that an item's visibility view returns an html string
|
||||
that does NOT contain the provided substrings.
|
||||
"""
|
||||
item = self.store.get_item(item_location)
|
||||
html = item.visibility_view().body_html()
|
||||
for string in substrings:
|
||||
self.assertNotIn(string, html)
|
||||
|
||||
def test_html_no_partition(self):
|
||||
self.verify_visibility_view_contains(self.video_location, 'No content groups exist')
|
||||
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
|
||||
|
||||
def test_html_empty_partition(self):
|
||||
self.create_content_groups([])
|
||||
self.verify_visibility_view_contains(self.video_location, 'No content groups exist')
|
||||
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
|
||||
|
||||
def test_html_populated_partition(self):
|
||||
self.create_content_groups(self.pet_groups)
|
||||
self.verify_visibility_view_contains(self.video_location, ['Cat Lovers', 'Dog Lovers'])
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers']
|
||||
)
|
||||
|
||||
self.verify_visibility_view_does_not_contain(
|
||||
self.video_location,
|
||||
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.ENROLLMENT_GROUPS_TITLE]
|
||||
)
|
||||
|
||||
def test_html_no_partition_staff_locked(self):
|
||||
self.set_staff_only(self.vertical_location)
|
||||
self.verify_visibility_view_contains(self.video_location, ['No content groups exist'])
|
||||
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
|
||||
self.verify_visibility_view_does_not_contain(
|
||||
self.video_location,
|
||||
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, self.ENROLLMENT_GROUPS_TITLE]
|
||||
)
|
||||
|
||||
def test_html_empty_partition_staff_locked(self):
|
||||
self.create_content_groups([])
|
||||
self.set_staff_only(self.vertical_location)
|
||||
self.verify_visibility_view_contains(self.video_location, 'No content groups exist')
|
||||
self.verify_visibility_view_contains(self.video_location, [self.NO_CONTENT_OR_ENROLLMENT_GROUPS])
|
||||
self.verify_visibility_view_does_not_contain(
|
||||
self.video_location,
|
||||
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, self.ENROLLMENT_GROUPS_TITLE]
|
||||
)
|
||||
|
||||
def test_html_populated_partition_staff_locked(self):
|
||||
self.create_content_groups(self.pet_groups)
|
||||
self.set_staff_only(self.vertical_location)
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
['The Unit this component is contained in is hidden from students.', 'Cat Lovers', 'Dog Lovers']
|
||||
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers']
|
||||
)
|
||||
|
||||
def test_html_false_content_group(self):
|
||||
self.create_content_groups(self.pet_groups)
|
||||
self.set_group_access(self.video_location, ['false_group_id'])
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location, ['Cat Lovers', 'Dog Lovers', 'Content group no longer exists.']
|
||||
self.video_location,
|
||||
[self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers', self.GROUP_NO_LONGER_EXISTS]
|
||||
)
|
||||
self.verify_visibility_view_does_not_contain(
|
||||
self.video_location,
|
||||
[self.STAFF_LOCKED]
|
||||
)
|
||||
|
||||
def test_html_false_content_group_staff_locked(self):
|
||||
@@ -145,18 +173,84 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
|
||||
[
|
||||
'Cat Lovers',
|
||||
'Dog Lovers',
|
||||
'The Unit this component is contained in is hidden from students.',
|
||||
'Content group no longer exists.'
|
||||
self.STAFF_LOCKED,
|
||||
self.GROUP_NO_LONGER_EXISTS
|
||||
]
|
||||
)
|
||||
|
||||
def test_html_verification_checkpoints(self):
|
||||
self.create_verification_user_partitions(["Midterm A", "Midterm B"])
|
||||
@override_settings(FEATURES=FEATURES_WITH_ENROLLMENT_TRACK_DISABLED)
|
||||
def test_enrollment_tracks_disabled(self):
|
||||
"""
|
||||
Test that the "no groups" messages doesn't reference enrollment tracks if
|
||||
they are disabled.
|
||||
"""
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.NO_CONTENT_ENROLLMENT_TRACK_DISABLED]
|
||||
)
|
||||
self.verify_visibility_view_does_not_contain(self.video_location, [self.NO_CONTENT_ENROLLMENT_TRACK_ENABLED])
|
||||
|
||||
def test_enrollment_track_partitions_only(self):
|
||||
"""
|
||||
Test what is displayed with no content groups but 2 enrollment modes registered.
|
||||
In all the cases where no enrollment modes are explicitly added, only the default
|
||||
enrollment mode exists, and we do not show it as an option (unless the course staff
|
||||
member has previously selected it).
|
||||
"""
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[self.ENROLLMENT_GROUPS_TITLE, 'audit course', 'verified course']
|
||||
)
|
||||
self.verify_visibility_view_does_not_contain(
|
||||
self.video_location,
|
||||
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.CONTENT_GROUPS_TITLE]
|
||||
)
|
||||
|
||||
def test_enrollment_track_partitions_and_content_groups(self):
|
||||
"""
|
||||
Test what is displayed with both enrollment groups and content groups.
|
||||
"""
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
|
||||
self.create_content_groups(self.pet_groups)
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[
|
||||
"Verification Checkpoint",
|
||||
"Midterm A",
|
||||
"Midterm B",
|
||||
self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers',
|
||||
self.ENROLLMENT_GROUPS_TITLE, 'audit course', 'verified course'
|
||||
]
|
||||
)
|
||||
self.verify_visibility_view_does_not_contain(
|
||||
self.video_location,
|
||||
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS]
|
||||
)
|
||||
|
||||
def test_missing_enrollment_mode(self):
|
||||
"""
|
||||
Test that an enrollment mode that is no longer registered is displayed as 'deleted',
|
||||
regardless of the number of current enrollment modes in the course.
|
||||
"""
|
||||
# Only 1 mode (the default) exists, so nothing initially shows in the visibility view.
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[self.NO_CONTENT_OR_ENROLLMENT_GROUPS, self.NO_CONTENT_ENROLLMENT_TRACK_ENABLED]
|
||||
)
|
||||
self.verify_visibility_view_does_not_contain(
|
||||
self.video_location, [self.ENROLLMENT_GROUPS_TITLE, self.GROUP_NO_LONGER_EXISTS]
|
||||
)
|
||||
|
||||
# Set group_access to reference a missing mode.
|
||||
self.set_group_access(self.video_location, ['10'], ENROLLMENT_TRACK_PARTITION_ID)
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location, [self.ENROLLMENT_GROUPS_TITLE, self.GROUP_NO_LONGER_EXISTS]
|
||||
)
|
||||
|
||||
# Add 2 explicit enrollment modes.
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[self.ENROLLMENT_GROUPS_TITLE, 'audit course', 'verified course', self.GROUP_NO_LONGER_EXISTS]
|
||||
)
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// studio - elements - modal-window
|
||||
// ========================
|
||||
|
||||
@import 'edx-pattern-library-shims/base/variables';
|
||||
|
||||
// start with the view/body
|
||||
[class*="view-"] {
|
||||
|
||||
@@ -482,59 +484,59 @@
|
||||
// MODAL TYPE: component - visibility modal
|
||||
.xblock-visibility_view {
|
||||
|
||||
.visibility-controls-secondary {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
@include margin(($baseline*0.75), 0, 0, $baseline);
|
||||
// We don't wish the dialog to resize for the common case of 2 groups.
|
||||
min-height: 190px;
|
||||
|
||||
.visibility-header {
|
||||
padding-bottom: $baseline;
|
||||
margin-bottom: 0;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.visibility-controls-group {
|
||||
@extend %wipe-last-child;
|
||||
margin-bottom: $baseline;
|
||||
.current-visibility-title {
|
||||
font-weight: font-weight(semi-bold);
|
||||
|
||||
.icon {
|
||||
@include margin-right($baseline/8);
|
||||
}
|
||||
}
|
||||
|
||||
.group-select-title {
|
||||
font-weight: font-weight(semi-bold);
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.partition-visibility {
|
||||
padding-top: $baseline;
|
||||
}
|
||||
|
||||
// UI: form fields
|
||||
.list-fields {
|
||||
.partition-group-control {
|
||||
|
||||
padding-top: ($baseline/2);
|
||||
|
||||
.field {
|
||||
@extend %wipe-last-child;
|
||||
margin-bottom: ($baseline/4);
|
||||
|
||||
label {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
}
|
||||
|
||||
// UI: radio and checkbox inputs
|
||||
.field-radio, .field-checkbox {
|
||||
margin-top: ($baseline/4);
|
||||
|
||||
label {
|
||||
@include margin-left($baseline/4);
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-visibility-verification {
|
||||
.note {
|
||||
@extend %t-copy-sub2;
|
||||
@extend %t-regular;
|
||||
margin: 14px 0 0 24px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: content group has been removed
|
||||
.field-visibility-content-group.was-removed {
|
||||
// CASE: content or enrollment group has been removed
|
||||
.partition-group-visibility.was-removed {
|
||||
|
||||
.input-checkbox:checked ~ label {
|
||||
color: $color-error;
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
.note {
|
||||
@extend %t-copy-sub2;
|
||||
@extend %t-regular;
|
||||
display: block;
|
||||
color: $color-error;
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +700,7 @@
|
||||
}
|
||||
|
||||
// UI: staff lock section
|
||||
.edit-staff-lock, .edit-settings-timed-examination, .edit-verification-access {
|
||||
.edit-staff-lock, .edit-settings-timed-examination {
|
||||
|
||||
.checkbox-cosmetic .input-checkbox {
|
||||
@extend %cont-text-sr;
|
||||
@@ -730,13 +732,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.verification-access {
|
||||
.checkbox-cosmetic .label {
|
||||
@include float(left);
|
||||
margin: 2px 6px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
// UI: timed and proctored exam section
|
||||
.edit-settings-timed-examination {
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangoapps.credit.partition_schemes import VerificationPartitionScheme
|
||||
from contentstore.utils import ancestor_has_staff_lock, get_visibility_partition_info
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
partition_info = get_visibility_partition_info(xblock)
|
||||
user_partitions = partition_info["user_partitions"]
|
||||
cohort_partitions = partition_info["cohort_partitions"]
|
||||
verification_partitions = partition_info["verification_partitions"]
|
||||
has_selected_groups = partition_info["has_selected_groups"]
|
||||
selected_verified_partition_id = partition_info["selected_verified_partition_id"]
|
||||
selectable_partitions = partition_info["selectable_partitions"]
|
||||
selected_partition_index = partition_info["selected_partition_index"]
|
||||
selected_groups_label = partition_info["selected_groups_label"]
|
||||
|
||||
is_staff_locked = ancestor_has_staff_lock(xblock)
|
||||
%>
|
||||
|
||||
<div class="modal-section visibility-summary">
|
||||
% if len(user_partitions) == 0:
|
||||
% if len(selectable_partitions) == 0:
|
||||
<div class="is-not-configured has-actions">
|
||||
<h4 class="title">${_('No content groups exist')}</h4>
|
||||
|
||||
<h3 class="title">${_('No visibility settings')}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('Use content groups to give groups of students access to a specific set of course content. Create one or more content groups, and make specific components visible to them.')}</p>
|
||||
<p>${_('No visibility settings are defined for this component, but visibility might be affected by inherited settings.')}</p>
|
||||
% if settings.FEATURES.get('ENABLE_ENROLLMENT_TRACK_USER_PARTITION'):
|
||||
<p>${_('You can make this component visible only to specific groups of learners based either on their enrollment track, or by content groups that you create.')}</p>
|
||||
% else:
|
||||
<p>${_('You can make this component visible only to specific groups of learners based on content groups that you create.')}</p>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
@@ -31,11 +35,11 @@ is_staff_locked = ancestor_has_staff_lock(xblock)
|
||||
<span class="icon fa fa-exclamation-triangle" aria-hidden="true"></span>
|
||||
<p class="copy">
|
||||
## Translators: Any text between {screen_reader_start} and {screen_reader_end} is only read by screen readers and never shown in the browser.
|
||||
${_(
|
||||
"{screen_reader_start}Warning:{screen_reader_end} The Unit this component is contained in is hidden from students. Visibility settings here will be trumped by this."
|
||||
).format(
|
||||
screen_reader_start='<span class="sr">',
|
||||
screen_reader_end='</span>',
|
||||
${Text(_(
|
||||
"{screen_reader_start}Warning:{screen_reader_end} The unit that contains this component is hidden from learners. The unit setting overrides the component visibility settings defined here."
|
||||
)).format(
|
||||
screen_reader_start=HTML('<span class="sr">'),
|
||||
screen_reader_end=HTML('</span>'),
|
||||
)
|
||||
}
|
||||
</p>
|
||||
@@ -43,96 +47,66 @@ is_staff_locked = ancestor_has_staff_lock(xblock)
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% if len(user_partitions) > 0:
|
||||
% if len(selectable_partitions) > 0:
|
||||
<form class="visibility-controls-form" method="post" action="">
|
||||
|
||||
<div role="group" aria-labelledby="visibility-title">
|
||||
<div class="modal-section visibility-controls">
|
||||
<h3 class="modal-section-title">${_('Make visible to:')}</h3>
|
||||
|
||||
<div class="modal-section-content">
|
||||
|
||||
<section class="visibility-controls-primary">
|
||||
<div class="list-fields list-radio">
|
||||
<div class="field field-radio field-visibility-level">
|
||||
<input type="radio" id="visibility-level-all" name="visibility-level" value="" class="input input-radio visibility-level-all" ${'checked="checked"' if not has_selected_groups else ''} />
|
||||
<label for="visibility-level-all" class="label">${_('All Students and Staff')}</label>
|
||||
</div>
|
||||
|
||||
<div class="field field-radio field-visibility-level">
|
||||
<input type="radio" id="visibility-level-specific" name="visibility-level" value="" class="input input-radio visibility-level-specific" ${'checked="checked"' if has_selected_groups else ''} />
|
||||
<label for="visibility-level-specific" class="label">${_('Specific Content Groups')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="wrapper-visibility-specific">
|
||||
<section class="visibility-controls-secondary">
|
||||
<div class="visibility-controls-group">
|
||||
<h4 class="visibility-controls-title modal-subsection-title sr">${_('Content Groups')}</h4>
|
||||
<div class="list-fields list-checkbox">
|
||||
% for partition in cohort_partitions:
|
||||
% for group in partition["groups"]:
|
||||
<div class="field field-checkbox field-visibility-content-group ${'was-removed' if group["deleted"] else ''}">
|
||||
<input type="checkbox"
|
||||
id="visibility-content-group-${partition["id"]}-${group["id"]}"
|
||||
name="visibility-content-group"
|
||||
value="${partition["id"]}-${group["id"]}"
|
||||
class="input input-checkbox"
|
||||
${'checked="checked"' if group["selected"] else ''}
|
||||
/>
|
||||
% if group["deleted"]:
|
||||
<label for="visibility-content-group-${partition["id"]}-${group["id"]}" class="label">
|
||||
${_('Deleted Content Group')}
|
||||
<span class="note">${_('Content group no longer exists. Please choose another or allow access to All Students and staff')}</span>
|
||||
</label>
|
||||
% else:
|
||||
<label for="visibility-content-group-${partition["id"]}-${group["id"]}" class="label">${group["name"] | h}</label>
|
||||
% endif
|
||||
</div>
|
||||
% endfor
|
||||
% endfor
|
||||
|
||||
## Allow only one verification checkpoint to be selected at a time.
|
||||
% if verification_partitions:
|
||||
<div role="group" aria-labelledby="verification-access-title">
|
||||
<div id="verification-access-title" class="sr">${_('Verification Checkpoint')}</div>
|
||||
<div class="field field-checkbox field-visibility-verification">
|
||||
<input type="checkbox"
|
||||
id="verification-access-checkbox"
|
||||
name="verification-access-checkbox"
|
||||
class="input input-checkbox"
|
||||
value=""
|
||||
aria-describedby="verification-help-text"
|
||||
${'checked="checked"' if selected_verified_partition_id is not None else ''}
|
||||
/>
|
||||
<label for="verification-access-checkbox" class="label">
|
||||
${_('Verification Checkpoint')}:
|
||||
</label>
|
||||
|
||||
<label class="sr" for="verification-access-dropdown">
|
||||
${_('Verification checkpoint to complete')}
|
||||
</label>
|
||||
|
||||
<select id="verification-access-dropdown">
|
||||
% for partition in verification_partitions:
|
||||
<option
|
||||
value="${partition["id"]}"
|
||||
${ "selected" if partition["id"] == selected_verified_partition_id else ""}
|
||||
>${partition["name"]}</option>
|
||||
% endfor
|
||||
</select>
|
||||
|
||||
<div class="note" id="verification-help-text">
|
||||
${_("Learners who require verification must pass the selected checkpoint to see the content in this component. Learners who do not require verification see this content by default.")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="modal-section-title visibility-header" id="visibility-title">
|
||||
<span class="current-visibility-title">
|
||||
<span class="icon fa fa-eye" aria-hidden="true"></span>
|
||||
<span>${_('Currently visible to:')}</span>
|
||||
</span>
|
||||
% if selected_partition_index == -1:
|
||||
<span>${_('All Learners and Staff')}</span>
|
||||
% else:
|
||||
<span>${selected_groups_label}</span>
|
||||
% endif
|
||||
</h3>
|
||||
<div class="modal-section-content partition-visibility">
|
||||
<label class="group-select-title">${_('Change visibility to:')}
|
||||
<select>
|
||||
<option value="-1" selected ="selected">
|
||||
% if selected_partition_index == -1:
|
||||
${_('Choose one')}
|
||||
% else:
|
||||
${_('All Learners and Staff')}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</option>
|
||||
% for index, partition in enumerate(selectable_partitions):
|
||||
<option value="${partition["id"]}" id="visibility-partition-${partition["id"]}" ${'selected="selected"' if selected_partition_index == index else ''}}>
|
||||
${partition["name"]}
|
||||
</option>
|
||||
% endfor
|
||||
</select>
|
||||
</label>
|
||||
|
||||
% for index, partition in enumerate(selectable_partitions):
|
||||
<div role="group" aria-labelledby="partition-group-directions-${partition["id"]}" aria-describedby="visibility-partition-${partition["id"]}"
|
||||
class="partition-group-control partition-group-control-${partition["id"]} ${'is-hidden' if selected_partition_index != index else ''}">
|
||||
<div class="partition-group-directions" id="partition-group-directions-${partition["id"]}">${_('Select one or more groups:')}
|
||||
% for group in partition["groups"]:
|
||||
<div class="field partition-group-visibility partition-group-visibility-${partition["id"]} ${'was-removed' if group["deleted"] else ''}">
|
||||
<input type="checkbox"
|
||||
id="visibility-group-${partition["id"]}-${group["id"]}"
|
||||
name="visibility-group"
|
||||
value="${group["id"]}"
|
||||
class="input input-checkbox"
|
||||
${'checked="checked"' if group["selected"] else ''}
|
||||
/>
|
||||
% if group["deleted"]:
|
||||
<label for="visibility-group-${partition["id"]}-${group["id"]}" class="label">
|
||||
${_("Deleted Group")}
|
||||
<span class="note">${_('This group no longer exists. Choose another group or make this component visible to All Learners and Staff.')}</span>
|
||||
</label>
|
||||
% else:
|
||||
<label for="visibility-group-${partition["id"]}-${group["id"]}" class="label">${group["name"]}</label>
|
||||
% endif
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
% endif
|
||||
% endif
|
||||
@@ -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):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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)}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user