Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
e5035746
Commit
e5035746
authored
Mar 13, 2017
by
cahrens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Introduce EnrollmentTrackUserPartition.
TNL-6674
parent
b6ba57ee
Show whitespace changes
Inline
Side-by-side
Showing
53 changed files
with
896 additions
and
286 deletions
+896
-286
cms/djangoapps/contentstore/course_group_config.py
+4
-3
cms/djangoapps/contentstore/tests/test_utils.py
+4
-4
cms/djangoapps/contentstore/utils.py
+4
-3
cms/djangoapps/contentstore/views/preview.py
+15
-0
cms/djangoapps/contentstore/views/tests/test_item.py
+67
-34
cms/envs/bok_choy.py
+2
-0
cms/envs/common.py
+8
-0
cms/envs/test.py
+1
-0
common/djangoapps/course_modes/admin.py
+5
-9
common/lib/xmodule/xmodule/course_module.py
+0
-17
common/lib/xmodule/xmodule/modulestore/mongo/base.py
+5
-0
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+5
-1
common/lib/xmodule/xmodule/partitions/partitions.py
+14
-2
common/lib/xmodule/xmodule/partitions/partitions_service.py
+124
-25
common/lib/xmodule/xmodule/partitions/tests/test_partitions.py
+131
-30
common/lib/xmodule/xmodule/split_test_module.py
+7
-10
common/lib/xmodule/xmodule/tests/test_split_test_module.py
+25
-9
lms/djangoapps/ccx/tests/test_field_override_performance.py
+27
-27
lms/djangoapps/course_api/blocks/tests/test_api.py
+2
-2
lms/djangoapps/course_blocks/transformers/user_partitions.py
+2
-5
lms/djangoapps/courseware/access.py
+0
-7
lms/djangoapps/courseware/tests/test_access.py
+14
-4
lms/djangoapps/courseware/tests/test_group_access.py
+0
-28
lms/djangoapps/courseware/tests/test_views.py
+4
-4
lms/djangoapps/courseware/testutils.py
+9
-0
lms/djangoapps/grades/tests/test_tasks.py
+12
-12
lms/djangoapps/instructor/views/instructor_dashboard.py
+1
-1
lms/djangoapps/instructor_task/tasks_helper.py
+2
-2
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+1
-1
lms/djangoapps/lms_xblock/mixin.py
+5
-3
lms/djangoapps/lms_xblock/runtime.py
+1
-18
lms/djangoapps/lti_provider/tests/test_views.py
+20
-0
lms/envs/bok_choy.py
+2
-0
lms/envs/common.py
+15
-2
lms/envs/test.py
+2
-0
lms/urls.py
+1
-1
openedx/core/djangoapps/credit/verification_access.py
+1
-1
openedx/core/djangoapps/verified_track_content/__init__.py
+0
-0
openedx/core/djangoapps/verified_track_content/admin.py
+2
-2
openedx/core/djangoapps/verified_track_content/forms.py
+1
-1
openedx/core/djangoapps/verified_track_content/migrations/0001_initial.py
+0
-0
openedx/core/djangoapps/verified_track_content/migrations/0002_verifiedtrackcohortedcourse_verified_cohort_name.py
+0
-0
openedx/core/djangoapps/verified_track_content/migrations/__init__.py
+0
-0
openedx/core/djangoapps/verified_track_content/models.py
+2
-2
openedx/core/djangoapps/verified_track_content/partition_scheme.py
+138
-0
openedx/core/djangoapps/verified_track_content/tasks.py
+0
-0
openedx/core/djangoapps/verified_track_content/tests/__init__.py
+0
-0
openedx/core/djangoapps/verified_track_content/tests/test_forms.py
+1
-1
openedx/core/djangoapps/verified_track_content/tests/test_models.py
+20
-8
openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py
+182
-0
openedx/core/djangoapps/verified_track_content/tests/test_views.py
+5
-5
openedx/core/djangoapps/verified_track_content/views.py
+2
-2
setup.py
+1
-0
No files found.
cms/djangoapps/contentstore/course_group_config.py
View file @
e5035746
...
...
@@ -9,10 +9,11 @@ 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.split_test_module
import
get_split_user_partitions
from
openedx.core.djangoapps.course_groups.partition_scheme
import
get_cohorted_user_partition
MINIMUM_GROUP_ID
=
100
MINIMUM_GROUP_ID
=
MINIMUM_STATIC_PARTITION_ID
RANDOM_SCHEME
=
"random"
COHORT_SCHEME
=
"cohort"
...
...
@@ -84,7 +85,7 @@ class GroupConfiguration(object):
"""
Assign ids for the group_configuration's groups.
"""
used_ids
=
[
g
.
id
for
p
in
self
.
course
.
user_partitions
for
g
in
p
.
groups
]
used_ids
=
[
g
.
id
for
p
in
get_all_partitions_for_course
(
self
.
course
)
for
g
in
p
.
groups
]
# Assign ids to every group in configuration.
for
group
in
self
.
configuration
.
get
(
'groups'
,
[]):
if
group
.
get
(
'id'
)
is
None
:
...
...
@@ -96,7 +97,7 @@ class GroupConfiguration(object):
"""
Return a list of IDs that already in use.
"""
return
set
([
p
.
id
for
p
in
course
.
user_partitions
])
return
set
([
p
.
id
for
p
in
get_all_partitions_for_course
(
course
)
])
def
get_user_partition
(
self
):
"""
...
...
cms/djangoapps/contentstore/tests/test_utils.py
View file @
e5035746
...
...
@@ -493,12 +493,12 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
]
}
]
self
.
assertEqual
(
self
.
_get_partition_info
(),
expected
)
self
.
assertEqual
(
self
.
_get_partition_info
(
schemes
=
[
"cohort"
,
"random"
]
),
expected
)
# Update group access and expect that now one group is marked as selected.
self
.
_set_group_access
({
0
:
[
1
]})
expected
[
0
][
"groups"
][
1
][
"selected"
]
=
True
self
.
assertEqual
(
self
.
_get_partition_info
(),
expected
)
self
.
assertEqual
(
self
.
_get_partition_info
(
schemes
=
[
"cohort"
,
"random"
]
),
expected
)
def
test_deleted_groups
(
self
):
# Select a group that is not defined in the partition
...
...
@@ -546,7 +546,7 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
])
# Expect that the inactive scheme is excluded from the results
partitions
=
self
.
_get_partition_info
()
partitions
=
self
.
_get_partition_info
(
schemes
=
[
"cohort"
,
"verification"
]
)
self
.
assertEqual
(
len
(
partitions
),
1
)
self
.
assertEqual
(
partitions
[
0
][
"scheme"
],
"cohort"
)
...
...
@@ -572,7 +572,7 @@ class GetUserPartitionInfoTest(ModuleStoreTestCase):
])
# Expect that the partition with no groups is excluded from the results
partitions
=
self
.
_get_partition_info
()
partitions
=
self
.
_get_partition_info
(
schemes
=
[
"cohort"
,
"verification"
]
)
self
.
assertEqual
(
len
(
partitions
),
1
)
self
.
assertEqual
(
partitions
[
0
][
"scheme"
],
"verification"
)
...
...
cms/djangoapps/contentstore/utils.py
View file @
e5035746
...
...
@@ -14,6 +14,7 @@ from django_comment_common.utils import seed_permissions_roles
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
openedx.core.djangoapps.site_configuration.models
import
SiteConfiguration
from
xmodule.partitions.partitions_service
import
get_all_partitions_for_course
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
...
...
@@ -373,11 +374,11 @@ def get_user_partition_info(xblock, schemes=None, course=None):
schemes
=
set
(
schemes
)
partitions
=
[]
for
p
in
sorted
(
course
.
user_partitions
,
key
=
lambda
p
:
p
.
name
):
for
p
in
sorted
(
get_all_partitions_for_course
(
course
,
active_only
=
True
)
,
key
=
lambda
p
:
p
.
name
):
# Exclude disabled partitions, partitions with no groups defined
# Also filter by scheme name if there's a filter defined.
if
p
.
active
and
p
.
groups
and
(
schemes
is
None
or
p
.
scheme
.
name
in
schemes
):
if
p
.
groups
and
(
schemes
is
None
or
p
.
scheme
.
name
in
schemes
):
# First, add groups defined by the partition
groups
=
[]
...
...
@@ -408,7 +409,7 @@ def get_user_partition_info(xblock, schemes=None, course=None):
# Put together the entire partition dictionary
partitions
.
append
({
"id"
:
p
.
id
,
"name"
:
p
.
name
,
"name"
:
unicode
(
p
.
name
),
# Convert into a string in case ugettext_lazy was used
"scheme"
:
p
.
scheme
.
name
,
"groups"
:
groups
,
})
...
...
cms/djangoapps/contentstore/views/preview.py
View file @
e5035746
...
...
@@ -16,6 +16,7 @@ from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
from
xmodule.contentstore.django
import
contentstore
from
xmodule.error_module
import
ErrorDescriptor
from
xmodule.exceptions
import
NotFoundError
,
ProcessingError
from
xmodule.partitions.partitions_service
import
PartitionService
from
xmodule.studio_editable
import
has_author_view
from
xmodule.services
import
SettingsService
from
xmodule.modulestore.django
import
modulestore
,
ModuleI18nService
...
...
@@ -213,10 +214,24 @@ def _preview_module_system(request, descriptor, field_data):
"i18n"
:
ModuleI18nService
,
"settings"
:
SettingsService
(),
"user"
:
DjangoXBlockUserService
(
request
.
user
),
"partitions"
:
StudioPartitionService
(
course_id
=
course_id
)
},
)
class
StudioPartitionService
(
PartitionService
):
"""
A runtime mixin to allow the display and editing of component visibility based on user partitions.
"""
def
get_user_group_id_for_partition
(
self
,
user
,
user_partition_id
):
"""
Override this method to return None, as the split_test_module calls this
to determine which group a user should see, but is robust to getting a return
value of None meaning that all groups should be shown.
"""
return
None
def
_load_preview_module
(
request
,
descriptor
):
"""
Return a preview XModule instantiated from the supplied descriptor. Will use mutable fields
...
...
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
e5035746
...
...
@@ -8,6 +8,7 @@ from pytz import UTC
from
pyquery
import
PyQuery
from
webob
import
Response
from
django.conf
import
settings
from
django.http
import
Http404
from
django.test
import
TestCase
from
django.test.client
import
RequestFactory
...
...
@@ -44,6 +45,7 @@ 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
class
AsideTest
(
XBlockAside
):
...
...
@@ -341,15 +343,17 @@ class GetItemTest(ItemTest):
)
def
test_get_user_partitions_and_groups
(
self
):
# Note about UserPartition and UserPartition Group IDs: these must not conflict with IDs used
# by dynamic user partitions.
self
.
course
.
user_partitions
=
[
UserPartition
(
id
=
0
,
id
=
MINIMUM_STATIC_PARTITION_ID
,
name
=
"Verification user partition"
,
scheme
=
UserPartition
.
get_scheme
(
"verification"
),
description
=
"Verification user partition"
,
groups
=
[
Group
(
id
=
0
,
name
=
"Group A"
),
Group
(
id
=
1
,
name
=
"Group B"
),
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.
],
),
]
...
...
@@ -365,18 +369,31 @@ class GetItemTest(ItemTest):
result
=
json
.
loads
(
resp
.
content
)
self
.
assertEqual
(
result
[
"user_partitions"
],
[
{
"id"
:
0
,
"id"
:
ENROLLMENT_TRACK_PARTITION_ID
,
"name"
:
"Enrollment Track Partition"
,
"scheme"
:
"enrollment_track"
,
"groups"
:
[
{
"id"
:
settings
.
COURSE_ENROLLMENT_MODES
[
"audit"
],
"name"
:
"Audit"
,
"selected"
:
False
,
"deleted"
:
False
,
}
]
},
{
"id"
:
MINIMUM_STATIC_PARTITION_ID
,
"name"
:
"Verification user partition"
,
"scheme"
:
"verification"
,
"groups"
:
[
{
"id"
:
0
,
"id"
:
MINIMUM_STATIC_PARTITION_ID
+
1
,
"name"
:
"Group A"
,
"selected"
:
False
,
"deleted"
:
False
,
},
{
"id"
:
1
,
"id"
:
MINIMUM_STATIC_PARTITION_ID
+
2
,
"name"
:
"Group B"
,
"selected"
:
False
,
"deleted"
:
False
,
...
...
@@ -1704,15 +1721,30 @@ class TestEditSplitModule(ItemTest):
def
setUp
(
self
):
super
(
TestEditSplitModule
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
course
.
user_partitions
=
[
UserPartition
(
0
,
'first_partition'
,
'First Partition'
,
[
Group
(
"0"
,
'alpha'
),
Group
(
"1"
,
'beta'
)]
),
UserPartition
(
1
,
'second_partition'
,
'Second Partition'
,
[
Group
(
"0"
,
'Group 0'
),
Group
(
"1"
,
'Group 1'
),
Group
(
"2"
,
'Group 2'
)]
self
.
first_user_partition_group_1
=
Group
(
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
1
),
'alpha'
)
self
.
first_user_partition_group_2
=
Group
(
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
2
),
'beta'
)
self
.
first_user_partition
=
UserPartition
(
MINIMUM_STATIC_PARTITION_ID
,
'first_partition'
,
'First Partition'
,
[
self
.
first_user_partition_group_1
,
self
.
first_user_partition_group_2
]
)
# There is a test point below (test_create_groups) that purposefully wants the group IDs
# of the 2 partitions to overlap (which is not something that normally happens).
self
.
second_user_partition_group_1
=
Group
(
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
1
),
'Group 1'
)
self
.
second_user_partition_group_2
=
Group
(
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
2
),
'Group 2'
)
self
.
second_user_partition_group_3
=
Group
(
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
3
),
'Group 3'
)
self
.
second_user_partition
=
UserPartition
(
MINIMUM_STATIC_PARTITION_ID
+
10
,
'second_partition'
,
'Second Partition'
,
[
self
.
second_user_partition_group_1
,
self
.
second_user_partition_group_2
,
self
.
second_user_partition_group_3
]
)
self
.
course
.
user_partitions
=
[
self
.
first_user_partition
,
self
.
second_user_partition
]
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
root_usage_key
=
self
.
_create_vertical
()
...
...
@@ -1760,8 +1792,8 @@ class TestEditSplitModule(ItemTest):
self
.
assertEqual
(
-
1
,
split_test
.
user_partition_id
)
self
.
assertEqual
(
0
,
len
(
split_test
.
children
))
# Set the user_partition_id to
0
.
split_test
=
self
.
_update_partition_id
(
0
)
# Set the user_partition_id to
match the first user_partition
.
split_test
=
self
.
_update_partition_id
(
self
.
first_user_partition
.
id
)
# Verify that child verticals have been set to match the groups
self
.
assertEqual
(
2
,
len
(
split_test
.
children
))
...
...
@@ -1769,13 +1801,13 @@ class TestEditSplitModule(ItemTest):
vertical_1
=
self
.
get_item_from_modulestore
(
split_test
.
children
[
1
],
verify_is_draft
=
True
)
self
.
assertEqual
(
"vertical"
,
vertical_0
.
category
)
self
.
assertEqual
(
"vertical"
,
vertical_1
.
category
)
self
.
assertEqual
(
"Group ID
0"
,
vertical_0
.
display_name
)
self
.
assertEqual
(
"Group ID
1"
,
vertical_1
.
display_name
)
self
.
assertEqual
(
"Group ID
"
+
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
1
)
,
vertical_0
.
display_name
)
self
.
assertEqual
(
"Group ID
"
+
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
2
)
,
vertical_1
.
display_name
)
# Verify that the group_id_to_child mapping is correct.
self
.
assertEqual
(
2
,
len
(
split_test
.
group_id_to_child
))
self
.
assertEqual
(
vertical_0
.
location
,
split_test
.
group_id_to_child
[
'0'
])
self
.
assertEqual
(
vertical_1
.
location
,
split_test
.
group_id_to_child
[
'1'
])
self
.
assertEqual
(
vertical_0
.
location
,
split_test
.
group_id_to_child
[
str
(
self
.
first_user_partition_group_1
.
id
)
])
self
.
assertEqual
(
vertical_1
.
location
,
split_test
.
group_id_to_child
[
str
(
self
.
first_user_partition_group_2
.
id
)
])
def
test_split_xblock_info_group_name
(
self
):
"""
...
...
@@ -1785,8 +1817,8 @@ class TestEditSplitModule(ItemTest):
# Initially, no user_partition_id is set, and the split_test has no children.
self
.
assertEqual
(
split_test
.
user_partition_id
,
-
1
)
self
.
assertEqual
(
len
(
split_test
.
children
),
0
)
# Set the user_partition_id to
0
.
split_test
=
self
.
_update_partition_id
(
0
)
# Set the user_partition_id to
match the first user_partition
.
split_test
=
self
.
_update_partition_id
(
self
.
first_user_partition
.
id
)
# Verify that child verticals have been set to match the groups
self
.
assertEqual
(
len
(
split_test
.
children
),
2
)
...
...
@@ -1808,13 +1840,13 @@ class TestEditSplitModule(ItemTest):
group configuration.
"""
# Set to first group configuration.
split_test
=
self
.
_update_partition_id
(
0
)
split_test
=
self
.
_update_partition_id
(
self
.
first_user_partition
.
id
)
self
.
assertEqual
(
2
,
len
(
split_test
.
children
))
initial_vertical_0_location
=
split_test
.
children
[
0
]
initial_vertical_1_location
=
split_test
.
children
[
1
]
# Set to second group configuration
split_test
=
self
.
_update_partition_id
(
1
)
split_test
=
self
.
_update_partition_id
(
self
.
second_user_partition
.
id
)
# We don't remove existing children.
self
.
assertEqual
(
5
,
len
(
split_test
.
children
))
self
.
assertEqual
(
initial_vertical_0_location
,
split_test
.
children
[
0
])
...
...
@@ -1825,9 +1857,9 @@ class TestEditSplitModule(ItemTest):
# Verify that the group_id_to child mapping is correct.
self
.
assertEqual
(
3
,
len
(
split_test
.
group_id_to_child
))
self
.
assertEqual
(
vertical_0
.
location
,
split_test
.
group_id_to_child
[
'0'
])
self
.
assertEqual
(
vertical_1
.
location
,
split_test
.
group_id_to_child
[
'1'
])
self
.
assertEqual
(
vertical_2
.
location
,
split_test
.
group_id_to_child
[
'2'
])
self
.
assertEqual
(
vertical_0
.
location
,
split_test
.
group_id_to_child
[
str
(
self
.
second_user_partition_group_1
.
id
)
])
self
.
assertEqual
(
vertical_1
.
location
,
split_test
.
group_id_to_child
[
str
(
self
.
second_user_partition_group_2
.
id
)
])
self
.
assertEqual
(
vertical_2
.
location
,
split_test
.
group_id_to_child
[
str
(
self
.
second_user_partition_group_3
.
id
)
])
self
.
assertNotEqual
(
initial_vertical_0_location
,
vertical_0
.
location
)
self
.
assertNotEqual
(
initial_vertical_1_location
,
vertical_1
.
location
)
...
...
@@ -1836,12 +1868,12 @@ class TestEditSplitModule(ItemTest):
Test that nothing happens when the user_partition_id is set to the same value twice.
"""
# Set to first group configuration.
split_test
=
self
.
_update_partition_id
(
0
)
split_test
=
self
.
_update_partition_id
(
self
.
first_user_partition
.
id
)
self
.
assertEqual
(
2
,
len
(
split_test
.
children
))
initial_group_id_to_child
=
split_test
.
group_id_to_child
# Set again to first group configuration.
split_test
=
self
.
_update_partition_id
(
0
)
split_test
=
self
.
_update_partition_id
(
self
.
first_user_partition
.
id
)
self
.
assertEqual
(
2
,
len
(
split_test
.
children
))
self
.
assertEqual
(
initial_group_id_to_child
,
split_test
.
group_id_to_child
)
...
...
@@ -1852,7 +1884,7 @@ class TestEditSplitModule(ItemTest):
The user_partition_id will be updated, but children and group_id_to_child map will not change.
"""
# Set to first group configuration.
split_test
=
self
.
_update_partition_id
(
0
)
split_test
=
self
.
_update_partition_id
(
self
.
first_user_partition
.
id
)
self
.
assertEqual
(
2
,
len
(
split_test
.
children
))
initial_group_id_to_child
=
split_test
.
group_id_to_child
...
...
@@ -1869,13 +1901,14 @@ class TestEditSplitModule(ItemTest):
TODO: move tests that can go over to common after the mixed modulestore work is done. # pylint: disable=fixme
"""
# Set to first group configuration.
split_test
=
self
.
_update_partition_id
(
0
)
split_test
=
self
.
_update_partition_id
(
self
.
first_user_partition
.
id
)
# Add a group to the first group configuration.
new_group_id
=
"1002"
split_test
.
user_partitions
=
[
UserPartition
(
0
,
'first_partition'
,
'First Partition'
,
[
Group
(
"0"
,
'alpha'
),
Group
(
"1"
,
'beta'
),
Group
(
"2"
,
'pie'
)]
self
.
first_user_partition
.
id
,
'first_partition'
,
'First Partition'
,
[
self
.
first_user_partition_group_1
,
self
.
first_user_partition_group_2
,
Group
(
new_group_id
,
'pie'
)]
)
]
self
.
store
.
update_item
(
split_test
,
self
.
user
.
id
)
...
...
@@ -1896,7 +1929,7 @@ class TestEditSplitModule(ItemTest):
split_test
=
self
.
_assert_children
(
3
)
self
.
assertNotEqual
(
group_id_to_child
,
split_test
.
group_id_to_child
)
group_id_to_child
=
split_test
.
group_id_to_child
self
.
assertEqual
(
split_test
.
children
[
2
],
group_id_to_child
[
"2"
])
self
.
assertEqual
(
split_test
.
children
[
2
],
group_id_to_child
[
new_group_id
])
# Call add_missing_groups again -- it should be a no-op.
split_test
.
add_missing_groups
(
self
.
request
)
...
...
cms/envs/bok_choy.py
View file @
e5035746
...
...
@@ -93,6 +93,8 @@ FEATURES['LICENSING'] = True
FEATURES
[
'ENABLE_MOBILE_REST_API'
]
=
True
# Enable video bumper in Studio
FEATURES
[
'ENABLE_VIDEO_BUMPER'
]
=
True
# Enable video bumper in Studio settings
FEATURES
[
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
]
=
True
# Enable partner support link in Studio footer
PARTNER_SUPPORT_EMAIL
=
'partner-support@example.com'
...
...
cms/envs/common.py
View file @
e5035746
...
...
@@ -88,6 +88,8 @@ from lms.envs.common import (
# File upload defaults
FILE_UPLOAD_STORAGE_BUCKET_NAME
,
FILE_UPLOAD_STORAGE_PREFIX
,
COURSE_ENROLLMENT_MODES
)
from
path
import
Path
as
path
from
warnings
import
simplefilter
...
...
@@ -225,6 +227,9 @@ FEATURES = {
# Allow public account creation
'ALLOW_PUBLIC_ACCOUNT_CREATION'
:
True
,
# Whether or not the dynamic EnrollmentTrackUserPartition should be registered.
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
:
False
,
}
ENABLE_JASMINE
=
False
...
...
@@ -883,6 +888,9 @@ INSTALLED_APPS = (
# for managing course modes
'course_modes'
,
# Verified Track Content Cohorting (Beta feature that will hopefully be removed)
'openedx.core.djangoapps.verified_track_content'
,
# Dark-launching languages
'openedx.core.djangoapps.dark_lang'
,
...
...
cms/envs/test.py
View file @
e5035746
...
...
@@ -320,6 +320,7 @@ FEATURES['ENABLE_COURSEWARE_INDEX'] = True
FEATURES
[
'ENABLE_LIBRARY_INDEX'
]
=
True
SEARCH_ENGINE
=
"search.tests.mock_search_engine.MockSearchEngine"
FEATURES
[
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
]
=
True
# teams feature
FEATURES
[
'ENABLE_TEAMS'
]
=
True
...
...
common/djangoapps/course_modes/admin.py
View file @
e5035746
...
...
@@ -28,22 +28,18 @@ from course_modes.models import CourseMode, CourseModeExpirationConfig
# the verification deadline table won't exist.
from
lms.djangoapps.verify_student
import
models
as
verification_models
COURSE_MODE_SLUG_CHOICES
=
[(
mode_slug
,
mode_slug
)
for
mode_slug
in
settings
.
COURSE_ENROLLMENT_MODES
]
class
CourseModeForm
(
forms
.
ModelForm
):
"""
Admin form for adding a course mode.
"""
class
Meta
(
object
):
model
=
CourseMode
fields
=
'__all__'
COURSE_MODE_SLUG_CHOICES
=
(
[(
CourseMode
.
DEFAULT_MODE_SLUG
,
CourseMode
.
DEFAULT_MODE_SLUG
)]
+
[(
mode_slug
,
mode_slug
)
for
mode_slug
in
CourseMode
.
VERIFIED_MODES
]
+
[(
CourseMode
.
NO_ID_PROFESSIONAL_MODE
,
CourseMode
.
NO_ID_PROFESSIONAL_MODE
)]
+
[(
mode_slug
,
mode_slug
)
for
mode_slug
in
CourseMode
.
CREDIT_MODES
]
+
# need to keep legacy modes around for awhile
[(
CourseMode
.
DEFAULT_SHOPPINGCART_MODE_SLUG
,
CourseMode
.
DEFAULT_SHOPPINGCART_MODE_SLUG
)]
)
mode_slug
=
forms
.
ChoiceField
(
choices
=
COURSE_MODE_SLUG_CHOICES
,
label
=
_
(
"Mode"
))
# The verification deadline is stored outside the course mode in the verify_student app.
...
...
common/lib/xmodule/xmodule/course_module.py
View file @
e5035746
...
...
@@ -1341,23 +1341,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
"""
return
self
.
teams_configuration
.
get
(
'topics'
,
None
)
def
get_user_partitions_for_scheme
(
self
,
scheme
):
"""
Retrieve all user partitions defined in the course for a particular
partition scheme.
Arguments:
scheme (object): The user partition scheme.
Returns:
list of `UserPartition`
"""
return
[
p
for
p
in
self
.
user_partitions
if
p
.
scheme
==
scheme
]
def
set_user_partitions_for_scheme
(
self
,
partitions
,
scheme
):
"""
Set the user partitions for a particular scheme.
...
...
common/lib/xmodule/xmodule/modulestore/mongo/base.py
View file @
e5035746
...
...
@@ -47,6 +47,7 @@ from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from
xmodule.modulestore.edit_info
import
EditInfoRuntimeMixin
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
,
DuplicateCourseError
,
ReferentialIntegrityError
from
xmodule.modulestore.inheritance
import
InheritanceMixin
,
inherit_metadata
,
InheritanceKeyValueStore
from
xmodule.partitions.partitions_service
import
PartitionService
from
xmodule.modulestore.xml
import
CourseLocationManager
from
xmodule.modulestore.store_utilities
import
DETACHED_XBLOCK_TYPES
from
xmodule.services
import
SettingsService
...
...
@@ -934,6 +935,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
if
self
.
request_cache
:
services
[
"request_cache"
]
=
self
.
request_cache
services
[
"partitions"
]
=
PartitionService
(
course_key
)
system
=
CachingDescriptorSystem
(
modulestore
=
self
,
course_key
=
course_key
,
...
...
@@ -1346,6 +1349,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
if
self
.
user_service
:
services
[
"user"
]
=
self
.
user_service
services
[
"partitions"
]
=
PartitionService
(
course_key
)
runtime
=
CachingDescriptorSystem
(
modulestore
=
self
,
module_data
=
{},
...
...
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
View file @
e5035746
...
...
@@ -83,6 +83,7 @@ from xmodule.modulestore import (
from
..exceptions
import
ItemNotFoundError
from
.caching_descriptor_system
import
CachingDescriptorSystem
from
xmodule.partitions.partitions_service
import
PartitionService
from
xmodule.modulestore.split_mongo.mongo_connection
import
MongoConnection
,
DuplicateKeyError
from
xmodule.modulestore.split_mongo
import
BlockKey
,
CourseEnvelope
from
xmodule.modulestore.store_utilities
import
DETACHED_XBLOCK_TYPES
...
...
@@ -3359,6 +3360,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
"""
Create the proper runtime for this course
"""
services
=
self
.
services
services
[
"partitions"
]
=
PartitionService
(
course_entry
.
course_key
)
return
CachingDescriptorSystem
(
modulestore
=
self
,
course_entry
=
course_entry
,
...
...
@@ -3370,7 +3374,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
mixins
=
self
.
xblock_mixins
,
select
=
self
.
xblock_select
,
disabled_xblock_types
=
self
.
disabled_xblock_types
,
services
=
se
lf
.
se
rvices
,
services
=
services
,
)
def
ensure_indexes
(
self
):
...
...
common/lib/xmodule/xmodule/partitions/partitions.py
View file @
e5035746
...
...
@@ -127,7 +127,7 @@ class UserPartition(namedtuple("UserPartition", "id name description groups sche
try
:
scheme
=
UserPartition
.
scheme_extensions
[
name
]
.
plugin
except
KeyError
:
raise
UserPartitionError
(
"Unrecognized scheme
{0}
"
.
format
(
name
))
raise
UserPartitionError
(
"Unrecognized scheme
'{0}'
"
.
format
(
name
))
scheme
.
name
=
name
return
scheme
...
...
@@ -188,6 +188,16 @@ class UserPartition(namedtuple("UserPartition", "id name description groups sche
if
not
scheme
:
raise
TypeError
(
"UserPartition dict {0} has unrecognized scheme {1}"
.
format
(
value
,
scheme_id
))
if
hasattr
(
scheme
,
"create_user_partition"
):
return
scheme
.
create_user_partition
(
value
[
"id"
],
value
[
"name"
],
value
[
"description"
],
groups
,
parameters
,
active
,
)
else
:
return
UserPartition
(
value
[
"id"
],
value
[
"name"
],
...
...
@@ -214,5 +224,7 @@ class UserPartition(namedtuple("UserPartition", "id name description groups sche
return
group
raise
NoSuchUserPartitionGroupError
(
"could not find a Group with ID [{}] in UserPartition [{}]"
.
format
(
group_id
,
self
.
id
)
"Could not find a Group with ID [{group_id}] in UserPartition [{partition_id}]."
.
format
(
group_id
=
group_id
,
partition_id
=
self
.
id
)
)
common/lib/xmodule/xmodule/partitions/partitions_service.py
View file @
e5035746
...
...
@@ -3,31 +3,122 @@ This is a service-like API that assigns tracks which groups users are in for var
user partitions. It uses the user_service key/value store provided by the LMS runtime to
persist the assignments.
"""
from
abc
import
ABCMeta
,
abstractproperty
from
django.conf
import
settings
from
django.utils.translation
import
ugettext_lazy
as
_
import
logging
from
xmodule.partitions.partitions
import
UserPartition
,
UserPartitionError
from
xmodule.modulestore.django
import
modulestore
class
PartitionService
(
object
):
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'
,
{})
def
get_all_partitions_for_course
(
course
,
active_only
=
False
):
"""
This is an XBlock service that assigns tracks which groups users are in for various
user partitions. It uses the provided user_tags service object to
persist the assignments.
A method that returns all `UserPartitions` associated with a course, as a List.
This will include the ones defined in course.user_partitions, but it may also
include dynamically included partitions (such as the `EnrollmentTrackUserPartition`).
Args:
course: the course for which user partitions should be returned.
active_only: if `True`, only partitions with `active` set to True will be returned.
Returns:
A List of UserPartitions associated with the course.
"""
__metaclass__
=
ABCMeta
all_partitions
=
course
.
user_partitions
+
_get_dynamic_partitions
(
course
)
if
active_only
:
all_partitions
=
[
partition
for
partition
in
all_partitions
if
partition
.
active
]
return
all_partitions
@abstractproperty
def
course_partitions
(
self
):
def
_get_dynamic_partitions
(
course
):
"""
Return the set of partitions assigned to self._course_id
Return the dynamic user partitions for this course.
If none exists, returns an empty array.
"""
raise
NotImplementedError
(
'Subclasses must implement course_partition'
)
enrollment_partition
=
_create_enrollment_track_partition
(
course
)
return
[
enrollment_partition
]
if
enrollment_partition
else
[]
def
_create_enrollment_track_partition
(
course
):
"""
Create and return the dynamic enrollment track user partition.
If it cannot be created, None is returned.
"""
if
not
FEATURES
.
get
(
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
):
return
None
try
:
enrollment_track_scheme
=
UserPartition
.
get_scheme
(
"enrollment_track"
)
except
UserPartitionError
:
log
.
warning
(
"No 'enrollment_track' scheme registered, EnrollmentTrackUserPartition will not be created."
)
return
None
used_ids
=
set
(
p
.
id
for
p
in
course
.
user_partitions
)
if
ENROLLMENT_TRACK_PARTITION_ID
in
used_ids
:
# TODO: change to Exception after this has been in production for awhile, see TNL-6796.
log
.
warning
(
"Can't add 'enrollment_track' partition, as ID {id} is assigned to {partition} in course {course}."
.
format
(
id
=
ENROLLMENT_TRACK_PARTITION_ID
,
partition
=
_get_partition_from_id
(
course
.
user_partitions
,
ENROLLMENT_TRACK_PARTITION_ID
)
.
name
,
course
=
unicode
(
course
.
id
)
)
)
return
None
partition
=
enrollment_track_scheme
.
create_user_partition
(
id
=
ENROLLMENT_TRACK_PARTITION_ID
,
name
=
_
(
u"Enrollment Track Partition"
),
description
=
_
(
u"Partition for segmenting users by enrollment track"
),
parameters
=
{
"course_id"
:
unicode
(
course
.
id
)}
)
return
partition
def
__init__
(
self
,
user
,
course_id
,
track_function
=
None
,
cache
=
None
):
self
.
_user
=
user
class
PartitionService
(
object
):
"""
This is an XBlock service that returns information about the user partitions associated
with a given course.
"""
def
__init__
(
self
,
course_id
,
track_function
=
None
,
cache
=
None
):
self
.
_course_id
=
course_id
self
.
_track_function
=
track_function
self
.
_cache
=
cache
def
get_user_group_id_for_partition
(
self
,
user_partition_id
):
def
get_course
(
self
):
"""
Return the course instance associated with this PartitionService.
This default implementation looks up the course from the modulestore.
"""
return
modulestore
()
.
get_course
(
self
.
_course_id
)
@property
def
course_partitions
(
self
):
"""
Return the set of partitions assigned to self._course_id (both those set directly on the course
through course.user_partitions, and any dynamic partitions that exist). Note: this returns
both active and inactive partitions.
"""
return
get_all_partitions_for_course
(
self
.
get_course
())
def
get_user_group_id_for_partition
(
self
,
user
,
user_partition_id
):
"""
If the user is already assigned to a group in user_partition_id, return the
group_id.
...
...
@@ -35,9 +126,6 @@ class PartitionService(object):
If not, assign them to one of the groups, persist that decision, and
return the group_id.
If the group they are assigned to doesn't exist anymore, re-assign to one of
the existing groups and return its id.
Args:
user_partition_id -- an id of a partition that's hopefully in the
runtime.user_partitions list.
...
...
@@ -49,7 +137,7 @@ class PartitionService(object):
ValueError if the user_partition_id isn't found.
"""
cache_key
=
"PartitionService.ugidfp.{}.{}.{}"
.
format
(
self
.
_
user
.
id
,
self
.
_course_id
,
user_partition_id
user
.
id
,
self
.
_course_id
,
user_partition_id
)
if
self
.
_cache
and
(
cache_key
in
self
.
_cache
):
...
...
@@ -62,7 +150,7 @@ class PartitionService(object):
"in course {1}"
.
format
(
user_partition_id
,
self
.
_course_id
)
)
group
=
self
.
get_group
(
user_partition
)
group
=
self
.
get_group
(
user
,
user
_partition
)
group_id
=
group
.
id
if
group
else
None
if
self
.
_cache
is
not
None
:
...
...
@@ -73,22 +161,33 @@ class PartitionService(object):
def
_get_user_partition
(
self
,
user_partition_id
):
"""
Look for a user partition with a matching id in the course's partitions.
Note that this method can return an inactive user partition.
Returns:
A UserPartition, or None if not found.
"""
for
partition
in
self
.
course_partitions
:
if
partition
.
id
==
user_partition_id
:
return
partition
return
None
return
_get_partition_from_id
(
self
.
course_partitions
,
user_partition_id
)
def
get_group
(
self
,
user_partition
,
assign
=
True
):
def
get_group
(
self
,
user
,
user
_partition
,
assign
=
True
):
"""
Returns the group from the specified user partition to which the user is assigned.
If the user has not yet been assigned, a group will be chosen for them based upon
the partition's scheme.
"""
return
user_partition
.
scheme
.
get_group_for_user
(
self
.
_course_id
,
self
.
_
user
,
user_partition
,
assign
=
assign
,
track_function
=
self
.
_track_function
self
.
_course_id
,
user
,
user_partition
,
assign
=
assign
,
track_function
=
self
.
_track_function
)
def
_get_partition_from_id
(
partitions
,
user_partition_id
):
"""
Look for a user partition with a matching id in the provided list of partitions.
Returns:
A UserPartition, or None if not found.
"""
for
partition
in
partitions
:
if
partition
.
id
==
user_partition_id
:
return
partition
return
None
common/lib/xmodule/xmodule/partitions/tests/test_partitions.py
View file @
e5035746
...
...
@@ -6,12 +6,14 @@ Test the partitions and partitions service
from
unittest
import
TestCase
from
mock
import
Mock
from
opaque_keys.edx.locat
ions
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locat
or
import
CourseLocator
from
stevedore.extension
import
Extension
,
ExtensionManager
from
xmodule.partitions.partitions
import
(
Group
,
UserPartition
,
UserPartitionError
,
NoSuchUserPartitionGroupError
,
USER_PARTITION_SCHEME_NAMESPACE
)
from
xmodule.partitions.partitions_service
import
PartitionService
from
xmodule.partitions.partitions_service
import
(
PartitionService
,
get_all_partitions_for_course
,
ENROLLMENT_TRACK_PARTITION_ID
,
FEATURES
)
class
TestGroup
(
TestCase
):
...
...
@@ -105,6 +107,15 @@ class MockUserPartitionScheme(object):
return
groups
[
0
]
class
MockEnrollmentTrackUserPartitionScheme
(
MockUserPartitionScheme
):
def
create_user_partition
(
self
,
id
,
name
,
description
,
groups
=
None
,
parameters
=
None
,
active
=
True
):
# pylint: disable=redefined-builtin, invalid-name, unused-argument
"""
The EnrollmentTrackPartitionScheme provides this method to return a subclass of UserPartition.
"""
return
UserPartition
(
id
,
name
,
description
,
groups
,
self
,
parameters
,
active
)
class
PartitionTestCase
(
TestCase
):
"""Base class for test cases that require partitions"""
TEST_ID
=
0
...
...
@@ -113,12 +124,14 @@ class PartitionTestCase(TestCase):
TEST_PARAMETERS
=
{
"location"
:
"block-v1:edX+DemoX+Demo+type@block@uuid"
}
TEST_GROUPS
=
[
Group
(
0
,
'Group 1'
),
Group
(
1
,
'Group 2'
)]
TEST_SCHEME_NAME
=
"mock"
ENROLLMENT_TRACK_SCHEME_NAME
=
"enrollment_track"
def
setUp
(
self
):
super
(
PartitionTestCase
,
self
)
.
setUp
()
# Set up two user partition schemes: mock and random
self
.
non_random_scheme
=
MockUserPartitionScheme
(
self
.
TEST_SCHEME_NAME
)
self
.
random_scheme
=
MockUserPartitionScheme
(
"random"
)
self
.
enrollment_track_scheme
=
MockEnrollmentTrackUserPartitionScheme
(
self
.
ENROLLMENT_TRACK_SCHEME_NAME
)
extensions
=
[
Extension
(
self
.
non_random_scheme
.
name
,
USER_PARTITION_SCHEME_NAMESPACE
,
self
.
non_random_scheme
,
None
...
...
@@ -126,6 +139,9 @@ class PartitionTestCase(TestCase):
Extension
(
self
.
random_scheme
.
name
,
USER_PARTITION_SCHEME_NAMESPACE
,
self
.
random_scheme
,
None
),
Extension
(
self
.
enrollment_track_scheme
.
name
,
USER_PARTITION_SCHEME_NAMESPACE
,
self
.
enrollment_track_scheme
,
None
),
]
UserPartition
.
scheme_extensions
=
ExtensionManager
.
make_test_instance
(
extensions
,
namespace
=
USER_PARTITION_SCHEME_NAMESPACE
...
...
@@ -394,45 +410,50 @@ class TestUserPartition(PartitionTestCase):
self
.
assertEqual
(
partition
.
name
,
self
.
TEST_NAME
)
class
Static
PartitionService
(
PartitionService
):
class
Mock
PartitionService
(
PartitionService
):
"""
Mock PartitionService for testing.
"""
def
__init__
(
self
,
partitions
,
**
kwargs
):
super
(
Static
PartitionService
,
self
)
.
__init__
(
**
kwargs
)
self
.
_
partitions
=
partitions
def
__init__
(
self
,
course
,
**
kwargs
):
super
(
Mock
PartitionService
,
self
)
.
__init__
(
**
kwargs
)
self
.
_
course
=
course
@property
def
course_partitions
(
self
):
return
self
.
_partitions
def
get_course
(
self
):
return
self
.
_course
class
TestPartitionService
(
PartitionTestCase
):
class
PartitionServiceBaseClass
(
PartitionTestCase
):
"""
Test getting a user's group out of a partition
Base test class for testing the PartitionService.
"""
def
setUp
(
self
):
super
(
TestPartitionService
,
self
)
.
setUp
()
self
.
course
=
Mock
(
id
=
SlashSeparatedCourseKey
(
'org_0'
,
'course_0'
,
'run_0'
))
super
(
PartitionServiceBaseClass
,
self
)
.
setUp
()
self
.
course
=
Mock
(
id
=
CourseLocator
(
'org_0'
,
'course_0'
,
'run_0'
))
self
.
partition_service
=
self
.
_create_service
(
"ma"
)
def
_create_service
(
self
,
username
,
cache
=
None
):
"""Convenience method to generate a
Static
PartitionService for a user."""
"""Convenience method to generate a
Mock
PartitionService for a user."""
# Derive a "user_id" from the username, just so we don't have to add an
# extra param to this method. Just has to be unique per user.
user_id
=
abs
(
hash
(
username
))
return
StaticPartitionService
(
[
self
.
user_partition
],
user
=
Mock
(
self
.
user
=
Mock
(
username
=
username
,
email
=
'{}@edx.org'
.
format
(
username
),
is_staff
=
False
,
is_active
=
True
,
id
=
user_id
),
)
self
.
course
.
user_partitions
=
[
self
.
user_partition
]
return
MockPartitionService
(
self
.
course
,
course_id
=
self
.
course
.
id
,
track_function
=
Mock
(),
cache
=
cache
)
class
TestPartitionService
(
PartitionServiceBaseClass
):
"""
Test getting a user's group out of a partition
"""
def
test_get_user_group_id_for_partition
(
self
):
# assign the first group to be returned
user_partition_id
=
self
.
user_partition
.
id
...
...
@@ -440,12 +461,12 @@ class TestPartitionService(PartitionTestCase):
self
.
user_partition
.
scheme
.
current_group
=
groups
[
0
]
# get a group assigned to the user
group1_id
=
self
.
partition_service
.
get_user_group_id_for_partition
(
user_partition_id
)
group1_id
=
self
.
partition_service
.
get_user_group_id_for_partition
(
self
.
user
,
user_partition_id
)
self
.
assertEqual
(
group1_id
,
groups
[
0
]
.
id
)
# switch to the second group and verify that it is returned for the user
self
.
user_partition
.
scheme
.
current_group
=
groups
[
1
]
group2_id
=
self
.
partition_service
.
get_user_group_id_for_partition
(
user_partition_id
)
group2_id
=
self
.
partition_service
.
get_user_group_id_for_partition
(
self
.
user
,
user_partition_id
)
self
.
assertEqual
(
group2_id
,
groups
[
1
]
.
id
)
def
test_caching
(
self
):
...
...
@@ -453,14 +474,14 @@ class TestPartitionService(PartitionTestCase):
user_partition_id
=
self
.
user_partition
.
id
shared_cache
=
{}
# Two
Static
PartitionService objects that share the same cache:
# Two
Mock
PartitionService objects that share the same cache:
ps_shared_cache_1
=
self
.
_create_service
(
username
,
shared_cache
)
ps_shared_cache_2
=
self
.
_create_service
(
username
,
shared_cache
)
# A
Static
PartitionService with its own local cache
# A
Mock
PartitionService with its own local cache
ps_diff_cache
=
self
.
_create_service
(
username
,
{})
# A
Static
PartitionService that never uses caching.
# A
Mock
PartitionService that never uses caching.
ps_uncached
=
self
.
_create_service
(
username
)
# Set the group we expect users to be placed into
...
...
@@ -472,7 +493,7 @@ class TestPartitionService(PartitionTestCase):
for
part_svc
in
[
ps_shared_cache_1
,
ps_diff_cache
,
ps_uncached
]:
self
.
assertEqual
(
first_group
.
id
,
part_svc
.
get_user_group_id_for_partition
(
user_partition_id
)
part_svc
.
get_user_group_id_for_partition
(
self
.
user
,
user_partition_id
)
)
# Now select a new target group
...
...
@@ -485,20 +506,20 @@ class TestPartitionService(PartitionTestCase):
for
part_svc
in
[
ps_shared_cache_1
,
ps_shared_cache_2
,
ps_diff_cache
]:
self
.
assertEqual
(
first_group
.
id
,
part_svc
.
get_user_group_id_for_partition
(
user_partition_id
)
part_svc
.
get_user_group_id_for_partition
(
self
.
user
,
user_partition_id
)
)
# Our uncached service should be accurate.
self
.
assertEqual
(
second_group
.
id
,
ps_uncached
.
get_user_group_id_for_partition
(
user_partition_id
)
ps_uncached
.
get_user_group_id_for_partition
(
self
.
user
,
user_partition_id
)
)
# And a newly created service should see the right thing
ps_new_cache
=
self
.
_create_service
(
username
,
{})
self
.
assertEqual
(
second_group
.
id
,
ps_new_cache
.
get_user_group_id_for_partition
(
user_partition_id
)
ps_new_cache
.
get_user_group_id_for_partition
(
self
.
user
,
user_partition_id
)
)
def
test_get_group
(
self
):
...
...
@@ -509,10 +530,90 @@ class TestPartitionService(PartitionTestCase):
# assign first group and verify that it is returned for the user
self
.
user_partition
.
scheme
.
current_group
=
groups
[
0
]
group1
=
self
.
partition_service
.
get_group
(
self
.
user_partition
)
group1
=
self
.
partition_service
.
get_group
(
self
.
user
,
self
.
user
_partition
)
self
.
assertEqual
(
group1
,
groups
[
0
])
# switch to the second group and verify that it is returned for the user
self
.
user_partition
.
scheme
.
current_group
=
groups
[
1
]
group2
=
self
.
partition_service
.
get_group
(
self
.
user_partition
)
group2
=
self
.
partition_service
.
get_group
(
self
.
user
,
self
.
user
_partition
)
self
.
assertEqual
(
group2
,
groups
[
1
])
class
TestGetCourseUserPartitions
(
PartitionServiceBaseClass
):
"""
Test the helper method get_all_partitions_for_course.
"""
def
setUp
(
self
):
super
(
TestGetCourseUserPartitions
,
self
)
.
setUp
()
# django.conf.settings is not available when nosetests are run
TestGetCourseUserPartitions
.
_enable_enrollment_track_partition
(
True
)
@staticmethod
def
_enable_enrollment_track_partition
(
enable
):
"""
Enable or disable the feature flag for the enrollment track user partition.
"""
FEATURES
[
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
]
=
enable
def
test_enrollment_track_partition_added
(
self
):
"""
Test that the dynamic enrollment track scheme is added if there is no conflict with the user partition ID.
"""
all_partitions
=
get_all_partitions_for_course
(
self
.
course
)
self
.
assertEqual
(
2
,
len
(
all_partitions
))
self
.
assertEqual
(
self
.
TEST_SCHEME_NAME
,
all_partitions
[
0
]
.
scheme
.
name
)
enrollment_track_partition
=
all_partitions
[
1
]
self
.
assertEqual
(
self
.
ENROLLMENT_TRACK_SCHEME_NAME
,
enrollment_track_partition
.
scheme
.
name
)
self
.
assertEqual
(
unicode
(
self
.
course
.
id
),
enrollment_track_partition
.
parameters
[
'course_id'
])
self
.
assertEqual
(
ENROLLMENT_TRACK_PARTITION_ID
,
enrollment_track_partition
.
id
)
def
test_enrollment_track_partition_not_added_if_conflict
(
self
):
"""
Test that the dynamic enrollment track scheme is NOT added if a UserPartition exists with that ID.
"""
self
.
user_partition
=
UserPartition
(
ENROLLMENT_TRACK_PARTITION_ID
,
self
.
TEST_NAME
,
self
.
TEST_DESCRIPTION
,
self
.
TEST_GROUPS
,
self
.
non_random_scheme
,
self
.
TEST_PARAMETERS
,
)
self
.
course
.
user_partitions
=
[
self
.
user_partition
]
all_partitions
=
get_all_partitions_for_course
(
self
.
course
)
self
.
assertEqual
(
1
,
len
(
all_partitions
))
self
.
assertEqual
(
self
.
TEST_SCHEME_NAME
,
all_partitions
[
0
]
.
scheme
.
name
)
def
test_enrollment_track_partition_not_added_if_disabled
(
self
):
"""
Test that the dynamic enrollment track scheme is NOT added if the settings FEATURE flag is disabled.
"""
TestGetCourseUserPartitions
.
_enable_enrollment_track_partition
(
False
)
all_partitions
=
get_all_partitions_for_course
(
self
.
course
)
self
.
assertEqual
(
1
,
len
(
all_partitions
))
self
.
assertEqual
(
self
.
TEST_SCHEME_NAME
,
all_partitions
[
0
]
.
scheme
.
name
)
def
test_filter_inactive_user_partitions
(
self
):
"""
Tests supplying the `active_only` parameter.
"""
self
.
user_partition
=
UserPartition
(
self
.
TEST_ID
,
self
.
TEST_NAME
,
self
.
TEST_DESCRIPTION
,
self
.
TEST_GROUPS
,
self
.
non_random_scheme
,
self
.
TEST_PARAMETERS
,
active
=
False
)
self
.
course
.
user_partitions
=
[
self
.
user_partition
]
all_partitions
=
get_all_partitions_for_course
(
self
.
course
,
active_only
=
True
)
self
.
assertEqual
(
1
,
len
(
all_partitions
))
self
.
assertEqual
(
self
.
ENROLLMENT_TRACK_SCHEME_NAME
,
all_partitions
[
0
]
.
scheme
.
name
)
all_partitions
=
get_all_partitions_for_course
(
self
.
course
,
active_only
=
False
)
self
.
assertEqual
(
2
,
len
(
all_partitions
))
self
.
assertEqual
(
self
.
TEST_SCHEME_NAME
,
all_partitions
[
0
]
.
scheme
.
name
)
self
.
assertEqual
(
self
.
ENROLLMENT_TRACK_SCHEME_NAME
,
all_partitions
[
1
]
.
scheme
.
name
)
common/lib/xmodule/xmodule/split_test_module.py
View file @
e5035746
...
...
@@ -98,7 +98,8 @@ def get_split_user_partitions(user_partitions):
@XBlock.needs
(
'user_tags'
)
# pylint: disable=abstract-method
@XBlock.wants
(
'partitions'
)
@XBlock.needs
(
'partitions'
)
@XBlock.needs
(
'user'
)
class
SplitTestModule
(
SplitTestFields
,
XModule
,
StudioEditableModule
):
"""
Show the user the appropriate child. Uses the ExperimentState
...
...
@@ -193,9 +194,9 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
Returns the group ID, or None if none is available.
"""
partitions_service
=
self
.
runtime
.
service
(
self
,
'partitions'
)
if
not
partitions_service
:
return
None
return
partitions_service
.
get_user_group_id_for_partition
(
self
.
user_partition_id
)
user_service
=
self
.
runtime
.
service
(
self
,
'user'
)
user
=
user_service
.
_django_user
# pylint: disable=protected-access
return
partitions_service
.
get_user_group_id_for_partition
(
user
,
self
.
user_partition_id
)
@property
def
is_configured
(
self
):
...
...
@@ -370,8 +371,8 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
@XBlock.needs
(
'user_tags'
)
# pylint: disable=abstract-method
@XBlock.
want
s
(
'partitions'
)
@XBlock.
want
s
(
'user'
)
@XBlock.
need
s
(
'partitions'
)
@XBlock.
need
s
(
'user'
)
class
SplitTestDescriptor
(
SplitTestFields
,
SequenceDescriptor
,
StudioEditableDescriptor
):
# the editing interface can be the same as for sequences -- just a container
module_class
=
SplitTestModule
...
...
@@ -641,10 +642,6 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
Called from Studio view.
"""
user_service
=
self
.
runtime
.
service
(
self
,
'user'
)
if
user_service
is
None
:
return
Response
()
user_partition
=
self
.
get_selected_partition
()
changed
=
False
...
...
common/lib/xmodule/xmodule/tests/test_split_test_module.py
View file @
e5035746
...
...
@@ -6,7 +6,7 @@ import lxml
from
mock
import
Mock
,
patch
from
fs.memoryfs
import
MemoryFS
from
xmodule.partitions.tests.test_partitions
import
Static
PartitionService
,
PartitionTestCase
,
MockUserPartitionScheme
from
xmodule.partitions.tests.test_partitions
import
Mock
PartitionService
,
PartitionTestCase
,
MockUserPartitionScheme
from
xmodule.tests.xml
import
factories
as
xml
from
xmodule.tests.xml
import
XModuleXmlImportTest
from
xmodule.tests
import
get_test_system
...
...
@@ -14,6 +14,7 @@ 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
class
SplitTestModuleFactory
(
xml
.
XmlImportFactory
):
...
...
@@ -81,21 +82,30 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase):
self
.
module_system
.
descriptor_runtime
=
self
.
course
.
_runtime
# pylint: disable=protected-access
self
.
course
.
runtime
.
export_fs
=
MemoryFS
()
user
=
Mock
(
username
=
'ma'
,
email
=
'ma@edx.org'
,
is_staff
=
False
,
is_active
=
True
)
self
.
partitions_service
=
StaticPartitionService
(
[
# Create mock partition service, as these tests are running with XML in-memory system.
self
.
course
.
user_partitions
=
[
self
.
user_partition
,
UserPartition
(
1
,
'second_partition'
,
'Second Partition'
,
[
Group
(
"0"
,
'abel'
),
Group
(
"1"
,
'baker'
),
Group
(
"2"
,
'charlie'
)],
MINIMUM_STATIC_PARTITION_ID
,
'second_partition'
,
'Second Partition'
,
[
Group
(
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
1
),
'abel'
),
Group
(
unicode
(
MINIMUM_STATIC_PARTITION_ID
+
2
),
'baker'
),
Group
(
"103"
,
'charlie'
)
],
MockUserPartitionScheme
()
)
],
user
=
user
,
]
partitions_service
=
MockPartitionService
(
self
.
course
,
course_id
=
self
.
course
.
id
,
track_function
=
Mock
(
name
=
'track_function'
),
)
self
.
module_system
.
_services
[
'partitions'
]
=
self
.
partitions_service
# pylint: disable=protected-access
self
.
module_system
.
_services
[
'partitions'
]
=
partitions_service
# pylint: disable=protected-access
# Mock user_service user
user_service
=
Mock
()
user
=
Mock
(
username
=
'ma'
,
email
=
'ma@edx.org'
,
is_staff
=
False
,
is_active
=
True
)
user_service
.
_django_user
=
user
self
.
module_system
.
_services
[
'user'
]
=
user_service
# pylint: disable=protected-access
self
.
split_test_module
=
self
.
course_sequence
.
get_children
()[
0
]
self
.
split_test_module
.
bind_for_student
(
...
...
@@ -103,6 +113,12 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase):
user
.
id
)
# Create mock modulestore for getting the course. Needed for rendering the HTML
# view, since mock services exist and the rendering code will not short-circuit.
mocked_modulestore
=
Mock
()
mocked_modulestore
.
get_course
.
return_value
=
self
.
course
self
.
split_test_module
.
system
.
modulestore
=
mocked_modulestore
@ddt.ddt
class
SplitTestModuleLMSTest
(
SplitTestModuleTest
):
...
...
lms/djangoapps/ccx/tests/test_field_override_performance.py
View file @
e5035746
...
...
@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default,
# # of mongo queries,
# )
(
'no_overrides'
,
1
,
True
,
False
):
(
2
2
,
6
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
2
,
6
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
2
,
6
),
(
'ccx'
,
1
,
True
,
False
):
(
2
2
,
6
),
(
'ccx'
,
2
,
True
,
False
):
(
2
2
,
6
),
(
'ccx'
,
3
,
True
,
False
):
(
2
2
,
6
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
2
,
6
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
2
,
6
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
2
,
6
),
(
'ccx'
,
1
,
False
,
False
):
(
2
2
,
6
),
(
'ccx'
,
2
,
False
,
False
):
(
2
2
,
6
),
(
'ccx'
,
3
,
False
,
False
):
(
2
2
,
6
),
(
'no_overrides'
,
1
,
True
,
False
):
(
2
4
,
6
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
4
,
6
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
4
,
6
),
(
'ccx'
,
1
,
True
,
False
):
(
2
4
,
6
),
(
'ccx'
,
2
,
True
,
False
):
(
2
4
,
6
),
(
'ccx'
,
3
,
True
,
False
):
(
2
4
,
6
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
4
,
6
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
4
,
6
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
4
,
6
),
(
'ccx'
,
1
,
False
,
False
):
(
2
4
,
6
),
(
'ccx'
,
2
,
False
,
False
):
(
2
4
,
6
),
(
'ccx'
,
3
,
False
,
False
):
(
2
4
,
6
),
}
...
...
@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__
=
True
TEST_DATA
=
{
(
'no_overrides'
,
1
,
True
,
False
):
(
2
2
,
3
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
2
,
3
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
2
,
3
),
(
'ccx'
,
1
,
True
,
False
):
(
2
2
,
3
),
(
'ccx'
,
2
,
True
,
False
):
(
2
2
,
3
),
(
'ccx'
,
3
,
True
,
False
):
(
2
2
,
3
),
(
'ccx'
,
1
,
True
,
True
):
(
2
3
,
3
),
(
'ccx'
,
2
,
True
,
True
):
(
2
3
,
3
),
(
'ccx'
,
3
,
True
,
True
):
(
2
3
,
3
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
2
,
3
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
2
,
3
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
2
,
3
),
(
'ccx'
,
1
,
False
,
False
):
(
2
2
,
3
),
(
'ccx'
,
2
,
False
,
False
):
(
2
2
,
3
),
(
'ccx'
,
3
,
False
,
False
):
(
2
2
,
3
),
(
'no_overrides'
,
1
,
True
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
2
,
True
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
3
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
1
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
2
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
3
,
True
,
False
):
(
2
4
,
3
),
(
'ccx'
,
1
,
True
,
True
):
(
2
5
,
3
),
(
'ccx'
,
2
,
True
,
True
):
(
2
5
,
3
),
(
'ccx'
,
3
,
True
,
True
):
(
2
5
,
3
),
(
'no_overrides'
,
1
,
False
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
2
,
False
,
False
):
(
2
4
,
3
),
(
'no_overrides'
,
3
,
False
,
False
):
(
2
4
,
3
),
(
'ccx'
,
1
,
False
,
False
):
(
2
4
,
3
),
(
'ccx'
,
2
,
False
,
False
):
(
2
4
,
3
),
(
'ccx'
,
3
,
False
,
False
):
(
2
4
,
3
),
}
lms/djangoapps/course_api/blocks/tests/test_api.py
View file @
e5035746
...
...
@@ -146,7 +146,7 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
self
.
_get_blocks
(
course
,
expected_mongo_queries
=
0
,
expected_sql_queries
=
3
if
with_storage_backing
else
2
,
expected_sql_queries
=
5
if
with_storage_backing
else
4
,
)
@ddt.data
(
...
...
@@ -164,5 +164,5 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
self
.
_get_blocks
(
course
,
expected_mongo_queries
,
expected_sql_queries
=
1
1
if
with_storage_backing
else
3
,
expected_sql_queries
=
1
3
if
with_storage_backing
else
5
,
)
lms/djangoapps/course_blocks/transformers/user_partitions.py
View file @
e5035746
...
...
@@ -5,6 +5,7 @@ from openedx.core.djangoapps.content.block_structure.transformer import (
BlockStructureTransformer
,
FilteringTransformerMixin
,
)
from
xmodule.partitions.partitions_service
import
get_all_partitions_for_course
from
.split_test
import
SplitTestTransformer
from
.utils
import
get_field_on_block
...
...
@@ -46,11 +47,7 @@ class UserPartitionTransformer(FilteringTransformerMixin, BlockStructureTransfor
# Because user partitions are course-wide, only store data for
# them on the root block.
root_block
=
block_structure
.
get_xblock
(
block_structure
.
root_block_usage_key
)
user_partitions
=
[
user_partition
for
user_partition
in
getattr
(
root_block
,
'user_partitions'
,
[])
if
user_partition
.
active
]
user_partitions
=
get_all_partitions_for_course
(
root_block
,
active_only
=
True
)
block_structure
.
set_transformer_data
(
cls
,
'user_partitions'
,
user_partitions
)
# If there are no user partitions, this transformation is a
...
...
lms/djangoapps/courseware/access.py
View file @
e5035746
...
...
@@ -30,7 +30,6 @@ from xmodule.course_module import (
)
from
xmodule.error_module
import
ErrorDescriptor
from
xmodule.x_module
import
XModule
from
xmodule.split_test_module
import
get_split_user_partitions
from
xmodule.partitions.partitions
import
NoSuchUserPartitionError
,
NoSuchUserPartitionGroupError
from
courseware.access_response
import
(
...
...
@@ -466,12 +465,6 @@ def _has_group_access(descriptor, user, course_key):
This function returns a boolean indicating whether or not `user` has
sufficient group memberships to "load" a block (the `descriptor`)
"""
if
len
(
descriptor
.
user_partitions
)
==
len
(
get_split_user_partitions
(
descriptor
.
user_partitions
)):
# Short-circuit the process, since there are no defined user partitions that are not
# user_partitions used by the split_test module. The split_test module handles its own access
# via updating the children of the split_test module.
return
ACCESS_GRANTED
# Allow staff and instructors roles group access, as they are not masquerading as a student.
if
get_user_role
(
user
,
course_key
)
in
[
'staff'
,
'instructor'
]:
return
ACCESS_GRANTED
...
...
lms/djangoapps/courseware/tests/test_access.py
View file @
e5035746
...
...
@@ -45,6 +45,7 @@ from xmodule.course_module import (
)
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.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
...
...
@@ -301,9 +302,11 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
"""
Test that a user masquerading as a member of a group sees appropriate content in preview mode.
"""
partition_id
=
0
group_0_id
=
0
group_1_id
=
1
# Note about UserPartition and UserPartition Group IDs: these must not conflict with IDs used
# by dynamic user partitions.
partition_id
=
MINIMUM_STATIC_PARTITION_ID
group_0_id
=
MINIMUM_STATIC_PARTITION_ID
+
1
group_1_id
=
MINIMUM_STATIC_PARTITION_ID
+
2
user_partition
=
UserPartition
(
partition_id
,
'Test User Partition'
,
''
,
[
Group
(
group_0_id
,
'Group 1'
),
Group
(
group_1_id
,
'Group 2'
)],
...
...
@@ -314,7 +317,6 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
chapter
=
ItemFactory
.
create
(
category
=
"chapter"
,
parent_location
=
self
.
course
.
location
)
chapter
.
group_access
=
{
partition_id
:
[
group_0_id
]}
chapter
.
user_partitions
=
self
.
course
.
user_partitions
modulestore
()
.
update_item
(
self
.
course
,
ModuleStoreEnum
.
UserID
.
test
)
...
...
@@ -431,6 +433,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
user
=
Mock
()
descriptor
=
Mock
(
user_partitions
=
[])
descriptor
.
_class_tags
=
{}
descriptor
.
merged_group_access
=
{}
# Always returns true because DISABLE_START_DATES is set in test.py
self
.
assertTrue
(
access
.
_has_access_descriptor
(
user
,
'load'
,
descriptor
))
...
...
@@ -457,6 +460,8 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit
.
_class_tags
=
{}
# Needed for detached check in _has_access_descriptor
mock_unit
.
visible_to_staff_only
=
visible_to_staff_only
mock_unit
.
start
=
start
mock_unit
.
merged_group_access
=
{}
self
.
verify_access
(
mock_unit
,
expected_access
,
expected_error_type
)
def
test__has_access_descriptor_beta_user
(
self
):
...
...
@@ -465,6 +470,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit
.
days_early_for_beta
=
2
mock_unit
.
start
=
self
.
TOMORROW
mock_unit
.
visible_to_staff_only
=
False
mock_unit
.
merged_group_access
=
{}
self
.
assertTrue
(
bool
(
access
.
_has_access_descriptor
(
self
.
beta_user
,
'load'
,
mock_unit
,
course_key
=
self
.
course
.
id
)))
...
...
@@ -480,6 +486,8 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit
.
_class_tags
=
{}
# Needed for detached check in _has_access_descriptor
mock_unit
.
visible_to_staff_only
=
False
mock_unit
.
start
=
start
mock_unit
.
merged_group_access
=
{}
self
.
verify_access
(
mock_unit
,
True
)
@ddt.data
(
...
...
@@ -499,6 +507,8 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
mock_unit
.
_class_tags
=
{}
# Needed for detached check in _has_access_descriptor
mock_unit
.
visible_to_staff_only
=
False
mock_unit
.
start
=
start
mock_unit
.
merged_group_access
=
{}
self
.
verify_access
(
mock_unit
,
expected_access
,
expected_error_type
)
def
test__has_access_course_can_enroll
(
self
):
...
...
lms/djangoapps/courseware/tests/test_group_access.py
View file @
e5035746
...
...
@@ -406,31 +406,3 @@ class GroupAccessTestCase(ModuleStoreTestCase):
self
.
check_access
(
self
.
blue_dog
,
block_accessed
,
False
)
self
.
check_access
(
self
.
gray_worm
,
block_accessed
,
False
)
self
.
ensure_staff_access
(
block_accessed
)
def
test_group_access_short_circuits
(
self
):
"""
Test that the group_access check short-circuits if there are no user_partitions defined
except user_partitions in use by the split_test module.
"""
# Initially, "red_cat" user can't view the vertical.
self
.
set_group_access
(
self
.
chapter_location
,
{
self
.
animal_partition
.
id
:
[
self
.
dog_group
.
id
]})
self
.
check_access
(
self
.
red_cat
,
self
.
vertical_location
,
False
)
# Change the vertical's user_partitions value to the empty list. Now red_cat can view the vertical.
self
.
set_user_partitions
(
self
.
vertical_location
,
[])
self
.
check_access
(
self
.
red_cat
,
self
.
vertical_location
,
True
)
# Change the vertical's user_partitions value to include only "split_test" partitions.
split_test_partition
=
UserPartition
(
199
,
'split_test partition'
,
'nothing to look at here'
,
[
Group
(
2
,
'random group'
)],
scheme
=
UserPartition
.
get_scheme
(
"random"
),
)
self
.
set_user_partitions
(
self
.
vertical_location
,
[
split_test_partition
])
self
.
check_access
(
self
.
red_cat
,
self
.
vertical_location
,
True
)
# Finally, add back in a cohort user_partition
self
.
set_user_partitions
(
self
.
vertical_location
,
[
split_test_partition
,
self
.
animal_partition
])
self
.
check_access
(
self
.
red_cat
,
self
.
vertical_location
,
False
)
lms/djangoapps/courseware/tests/test_views.py
View file @
e5035746
...
...
@@ -206,7 +206,7 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS
=
20
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
9
),
(
ModuleStoreEnum
.
Type
.
mongo
,
10
),
(
ModuleStoreEnum
.
Type
.
split
,
4
),
)
@ddt.unpack
...
...
@@ -1420,17 +1420,17 @@ class ProgressPageTests(ModuleStoreTestCase):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration
(
enabled
=
self_paced_enabled
)
.
save
()
self
.
setup_course
(
self_paced
=
self_paced
)
with
self
.
assertNumQueries
(
39
),
check_mongo_calls
(
4
):
with
self
.
assertNumQueries
(
41
),
check_mongo_calls
(
4
):
self
.
_get_progress_page
()
def
test_progress_queries
(
self
):
self
.
setup_course
()
with
self
.
assertNumQueries
(
39
),
check_mongo_calls
(
4
):
with
self
.
assertNumQueries
(
41
),
check_mongo_calls
(
4
):
self
.
_get_progress_page
()
# subsequent accesses to the progress page require fewer queries.
for
_
in
range
(
2
):
with
self
.
assertNumQueries
(
2
5
),
check_mongo_calls
(
4
):
with
self
.
assertNumQueries
(
2
7
),
check_mongo_calls
(
4
):
self
.
_get_progress_page
()
@patch
(
...
...
lms/djangoapps/courseware/testutils.py
View file @
e5035746
...
...
@@ -178,6 +178,8 @@ class RenderXBlockTestMixin(object):
@ddt.unpack
def
test_success_enrolled_staff
(
self
,
default_store
,
mongo_calls
):
with
self
.
store
.
default_store
(
default_store
):
if
default_store
is
ModuleStoreEnum
.
Type
.
mongo
:
mongo_calls
=
self
.
get_success_enrolled_staff_mongo_count
()
self
.
setup_course
(
default_store
)
self
.
setup_user
(
admin
=
True
,
enroll
=
True
,
login
=
True
)
...
...
@@ -197,6 +199,13 @@ class RenderXBlockTestMixin(object):
with
check_mongo_calls
(
mongo_calls
):
self
.
verify_response
()
def
get_success_enrolled_staff_mongo_count
(
self
):
"""
Helper method used by test_success_enrolled_staff because one test
class using this mixin has an increased number of mongo (only) queries.
"""
return
5
def
test_success_unenrolled_staff
(
self
):
self
.
setup_course
()
self
.
setup_user
(
admin
=
True
,
enroll
=
False
,
login
=
True
)
...
...
lms/djangoapps/grades/tests/test_tasks.py
View file @
e5035746
...
...
@@ -154,10 +154,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
4
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
1
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
0
,
False
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
6
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
3
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
2
,
False
),
)
@ddt.unpack
def
test_query_counts
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
,
create_multiple_subsections
):
...
...
@@ -169,8 +169,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
4
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
6
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
),
)
@ddt.unpack
def
test_query_counts_dont_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
...
...
@@ -215,8 +215,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
9
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
8
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
11
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
10
),
)
@ddt.unpack
def
test_persistent_grades_not_enabled_on_course
(
self
,
default_store
,
num_mongo_queries
,
num_sql_queries
):
...
...
@@ -230,8 +230,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEqual
(
len
(
PersistentSubsectionGrade
.
bulk_read_grades
(
self
.
user
.
id
,
self
.
course
.
id
)),
0
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
2
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
4
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
),
)
@ddt.unpack
def
test_persistent_grades_enabled_on_course
(
self
,
default_store
,
num_mongo_queries
,
num_sql_queries
):
...
...
@@ -409,8 +409,8 @@ class ComputeGradesForCourseTest(HasCourseWithProblemsMixin, ModuleStoreTestCase
@ddt.data
(
*
xrange
(
1
,
12
,
3
))
def
test_database_calls
(
self
,
batch_size
):
per_user_queries
=
1
6
*
min
(
batch_size
,
6
)
# No more than 6 due to offset
with
self
.
assertNumQueries
(
3
+
16
*
min
(
batch_size
,
6
)
):
per_user_queries
=
1
8
*
min
(
batch_size
,
6
)
# No more than 6 due to offset
with
self
.
assertNumQueries
(
3
+
per_user_queries
):
with
check_mongo_calls
(
1
):
compute_grades_for_course
.
delay
(
course_key
=
six
.
text_type
(
self
.
course
.
id
),
...
...
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
e5035746
...
...
@@ -54,6 +54,7 @@ from class_dashboard.dashboard_data import get_section_display_name, get_array_s
from
.tools
import
get_units_with_due_date
,
title_or_url
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.verified_track_content.models
import
VerifiedTrackCohortedCourse
from
openedx.core.djangolib.markup
import
HTML
,
Text
...
...
@@ -640,7 +641,6 @@ def _section_send_email(course, access):
if
is_course_cohorted
(
course_key
):
cohorts
=
get_course_cohorts
(
course
)
course_modes
=
[]
from
verified_track_content.models
import
VerifiedTrackCohortedCourse
if
not
VerifiedTrackCohortedCourse
.
is_verified_track_cohort_enabled
(
course_key
):
course_modes
=
CourseMode
.
modes_for_course
(
course_key
,
include_expired
=
True
,
only_selectable
=
False
)
email_editor
=
fragment
.
content
...
...
lms/djangoapps/instructor_task/tasks_helper.py
View file @
e5035746
...
...
@@ -30,6 +30,7 @@ from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from
pytz
import
UTC
from
track
import
contexts
from
xmodule.modulestore.django
import
modulestore
from
xmodule.partitions.partitions_service
import
PartitionService
from
xmodule.split_test_module
import
get_split_user_partitions
from
certificates.api
import
generate_user_certificates
...
...
@@ -59,7 +60,6 @@ from shoppingcart.models import (
)
from
openassessment.data
import
OraAggregateData
from
lms.djangoapps.instructor_task.models
import
ReportStore
,
InstructorTask
,
PROGRESS
from
lms.djangoapps.lms_xblock.runtime
import
LmsPartitionService
from
openedx.core.djangoapps.course_groups.cohorts
import
get_cohort
from
openedx.core.djangoapps.course_groups.models
import
CourseUserGroup
from
opaque_keys.edx.keys
import
UsageKey
...
...
@@ -806,7 +806,7 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
group_configs_group_names
=
[]
for
partition
in
experiment_partitions
:
group
=
LmsPartitionService
(
student
,
course_id
)
.
get_group
(
partition
,
assign
=
False
)
group
=
PartitionService
(
course_id
)
.
get_group
(
student
,
partition
,
assign
=
False
)
group_configs_group_names
.
append
(
group
.
name
if
group
else
''
)
team_name
=
[]
...
...
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
e5035746
...
...
@@ -1775,7 +1775,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed'
:
3
,
'skipped'
:
2
}
with
self
.
assertNumQueries
(
1
68
):
with
self
.
assertNumQueries
(
1
84
):
self
.
assertCertificatesGenerated
(
task_input
,
expected_results
)
expected_results
=
{
...
...
lms/djangoapps/lms_xblock/mixin.py
View file @
e5035746
...
...
@@ -5,6 +5,7 @@ Namespace that defines fields common to all blocks used in the LMS
#from django.utils.translation import ugettext_noop as _
from
lazy
import
lazy
from
xblock.core
import
XBlock
from
xblock.fields
import
Boolean
,
Scope
,
String
,
XBlockMixin
,
Dict
from
xblock.validation
import
ValidationMessage
from
xmodule.modulestore.inheritance
import
UserPartitionList
...
...
@@ -26,6 +27,7 @@ class GroupAccessDict(Dict):
return
{
unicode
(
k
):
access_dict
[
k
]
for
k
in
access_dict
}
@XBlock.needs
(
'partitions'
)
class
LmsBlockMixin
(
XBlockMixin
):
"""
Mixin that defines fields common to all blocks used in the LMS
...
...
@@ -128,10 +130,10 @@ class LmsBlockMixin(XBlockMixin):
def
_get_user_partition
(
self
,
user_partition_id
):
"""
Returns the user partition with the specified id.
Raises
`NoSuchUserPartitionError` if the lookup fails.
Returns the user partition with the specified id.
Note that this method can return
an inactive user partition. Raises
`NoSuchUserPartitionError` if the lookup fails.
"""
for
user_partition
in
self
.
user
_partitions
:
for
user_partition
in
self
.
runtime
.
service
(
self
,
'partitions'
)
.
course
_partitions
:
if
user_partition
.
id
==
user_partition_id
:
return
user_partition
...
...
lms/djangoapps/lms_xblock/runtime.py
View file @
e5035746
...
...
@@ -80,22 +80,6 @@ def local_resource_url(block, uri):
return
xblock_local_resource_url
(
block
,
uri
)
class
LmsPartitionService
(
PartitionService
):
"""
Another runtime mixin that provides access to the student partitions defined on the
course.
(If and when XBlock directly provides access from one block (e.g. a split_test_module)
to another (e.g. a course_module), this won't be necessary, but for now it seems like
the least messy way to hook things through)
"""
@property
def
course_partitions
(
self
):
course
=
modulestore
()
.
get_course
(
self
.
_course_id
)
return
course
.
user_partitions
class
UserTagsService
(
object
):
"""
A runtime class that provides an interface to the user service. It handles filling in
...
...
@@ -154,8 +138,7 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method
services
[
'fs'
]
=
xblock
.
reference
.
plugins
.
FSService
()
services
[
'i18n'
]
=
ModuleI18nService
services
[
'library_tools'
]
=
LibraryToolsService
(
modulestore
())
services
[
'partitions'
]
=
LmsPartitionService
(
user
=
kwargs
.
get
(
'user'
),
services
[
'partitions'
]
=
PartitionService
(
course_id
=
kwargs
.
get
(
'course_id'
),
track_function
=
kwargs
.
get
(
'track_function'
,
None
),
cache
=
request_cache_dict
...
...
lms/djangoapps/lti_provider/tests/test_views.py
View file @
e5035746
...
...
@@ -170,6 +170,8 @@ class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCa
This class overrides the get_response method, which is used by
the tests defined in RenderXBlockTestMixin.
"""
SUCCESS_ENROLLED_STAFF_MONGO_COUNT
=
9
def
setUp
(
self
):
"""
Set up tests
...
...
@@ -212,3 +214,21 @@ class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCa
self
.
setup_course
()
self
.
setup_user
(
admin
=
False
,
enroll
=
True
,
login
=
False
)
self
.
verify_response
()
def
get_success_enrolled_staff_mongo_count
(
self
):
"""
Override because mongo queries are higher for this
particular test. This has not been investigated exhaustively
as mongo is no longer used much, and removing user_partitions
from inheritance fixes the problem.
# The 9 mongoDB calls include calls for
# Old Mongo:
# (1) fill_in_run
# (2) get_course in get_course_with_access
# (3) get_item for HTML block in get_module_by_usage_id
# (4) get_parent when loading HTML block
# (5)-(8) calls related to the inherited user_partitions field.
# (9) edx_notes descriptor call to get_course
"""
return
9
lms/envs/bok_choy.py
View file @
e5035746
...
...
@@ -146,6 +146,8 @@ FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# Open up endpoint for faking Software Secure responses
FEATURES
[
'ENABLE_SOFTWARE_SECURE_FAKE'
]
=
True
FEATURES
[
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
]
=
True
########################### Entrance Exams #################################
FEATURES
[
'ENTRANCE_EXAMS'
]
=
True
...
...
lms/envs/common.py
View file @
e5035746
...
...
@@ -371,6 +371,9 @@ FEATURES = {
# Enable footer banner for cookie consent.
# See https://cookieconsent.insites.com/ for more.
'ENABLE_COOKIE_CONSENT'
:
False
,
# Whether or not the dynamic EnrollmentTrackUserPartition should be registered.
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
:
False
,
}
# Ignore static asset files on import which match this pattern
...
...
@@ -2146,8 +2149,8 @@ INSTALLED_APPS = (
# API access administration
'openedx.core.djangoapps.api_admin'
,
# Verified Track Content Cohorting
'verified_track_content'
,
# Verified Track Content Cohorting
(Beta feature that will hopefully be removed)
'
openedx.core.djangoapps.
verified_track_content'
,
# Learner's dashboard
'learner_dashboard'
,
...
...
@@ -3068,3 +3071,13 @@ DOC_LINK_BASE_URL = None
ENTERPRISE_ENROLLMENT_API_URL
=
LMS_ROOT_URL
+
"/api/enrollment/v1/"
ENTERPRISE_PUBLIC_ENROLLMENT_API_URL
=
ENTERPRISE_ENROLLMENT_API_URL
ENTERPRISE_API_CACHE_TIMEOUT
=
3600
# Value is in seconds
############## Settings for Course Enrollment Modes ######################
COURSE_ENROLLMENT_MODES
=
{
"audit"
:
1
,
"verified"
:
2
,
"professional"
:
3
,
"no-id-professional"
:
4
,
"credit"
:
5
,
"honor"
:
6
,
}
lms/envs/test.py
View file @
e5035746
...
...
@@ -78,6 +78,8 @@ FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION'] = True
# Enable the milestones app in tests to be consistent with it being enabled in production
FEATURES
[
'MILESTONES_APP'
]
=
True
FEATURES
[
'ENABLE_ENROLLMENT_TRACK_USER_PARTITION'
]
=
True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED
=
True
...
...
lms/urls.py
View file @
e5035746
...
...
@@ -553,7 +553,7 @@ urlpatterns += (
r'^courses/{}/verified_track_content/settings'
.
format
(
settings
.
COURSE_KEY_PATTERN
,
),
'verified_track_content.views.cohorting_settings'
,
'
openedx.core.djangoapps.
verified_track_content.views.cohorting_settings'
,
name
=
'verified_track_cohorting'
,
),
url
(
...
...
openedx/core/djangoapps/credit/verification_access.py
View file @
e5035746
...
...
@@ -141,7 +141,7 @@ def _set_verification_partitions(course_key, icrv_blocks):
log
.
error
(
"Could not find course
%
s"
,
course_key
)
return
[]
verified_partitions
=
course
.
get_user_partitions_for_scheme
(
scheme
)
verified_partitions
=
[
p
for
p
in
course
.
user_partitions
if
p
.
scheme
==
scheme
]
partition_id_for_location
=
{
p
.
parameters
[
"location"
]:
p
.
id
for
p
in
verified_partitions
...
...
lms
/djangoapps/verified_track_content/__init__.py
→
openedx/core
/djangoapps/verified_track_content/__init__.py
View file @
e5035746
File moved
lms
/djangoapps/verified_track_content/admin.py
→
openedx/core
/djangoapps/verified_track_content/admin.py
View file @
e5035746
...
...
@@ -4,8 +4,8 @@ Django admin page for verified track configuration
from
django.contrib
import
admin
from
verified_track_content.forms
import
VerifiedTrackCourseForm
from
verified_track_content.models
import
VerifiedTrackCohortedCourse
from
openedx.core.djangoapps.
verified_track_content.forms
import
VerifiedTrackCourseForm
from
openedx.core.djangoapps.
verified_track_content.models
import
VerifiedTrackCohortedCourse
@admin.register
(
VerifiedTrackCohortedCourse
)
...
...
lms
/djangoapps/verified_track_content/forms.py
→
openedx/core
/djangoapps/verified_track_content/forms.py
View file @
e5035746
...
...
@@ -9,7 +9,7 @@ from xmodule.modulestore.django import modulestore
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
verified_track_content.models
import
VerifiedTrackCohortedCourse
from
openedx.core.djangoapps.
verified_track_content.models
import
VerifiedTrackCohortedCourse
class
VerifiedTrackCourseForm
(
forms
.
ModelForm
):
...
...
lms
/djangoapps/verified_track_content/migrations/0001_initial.py
→
openedx/core
/djangoapps/verified_track_content/migrations/0001_initial.py
View file @
e5035746
File moved
lms
/djangoapps/verified_track_content/migrations/0002_verifiedtrackcohortedcourse_verified_cohort_name.py
→
openedx/core
/djangoapps/verified_track_content/migrations/0002_verifiedtrackcohortedcourse_verified_cohort_name.py
View file @
e5035746
File moved
lms
/djangoapps/verified_track_content/migrations/__init__.py
→
openedx/core
/djangoapps/verified_track_content/migrations/__init__.py
View file @
e5035746
File moved
lms
/djangoapps/verified_track_content/models.py
→
openedx/core
/djangoapps/verified_track_content/models.py
View file @
e5035746
...
...
@@ -8,9 +8,9 @@ from django.db.models.signals import post_save, pre_save
from
openedx.core.djangoapps.xmodule_django.models
import
CourseKeyField
from
student.models
import
CourseEnrollment
from
courseware.courses
import
get_course_by_id
from
lms.djangoapps.
courseware.courses
import
get_course_by_id
from
verified_track_content.tasks
import
sync_cohort_with_mode
from
openedx.core.djangoapps.
verified_track_content.tasks
import
sync_cohort_with_mode
from
openedx.core.djangoapps.course_groups.cohorts
import
(
get_course_cohorts
,
CourseCohort
,
is_course_cohorted
,
get_random_cohort
)
...
...
openedx/core/djangoapps/verified_track_content/partition_scheme.py
0 → 100644
View file @
e5035746
"""
UserPartitionScheme for enrollment tracks.
"""
from
django.conf
import
settings
from
courseware.masquerade
import
(
get_course_masquerade
,
get_masquerading_group_info
,
is_masquerading_as_specific_student
,
)
from
course_modes.models
import
CourseMode
from
student.models
import
CourseEnrollment
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.verified_track_content.models
import
VerifiedTrackCohortedCourse
from
xmodule.partitions.partitions
import
NoSuchUserPartitionGroupError
,
Group
,
UserPartition
# These IDs must be less than 100 so that they do not overlap with Groups in
# CohortUserPartition or RandomUserPartitionScheme
# (CMS' course_group_config uses a minimum value of 100 for all generated IDs).
ENROLLMENT_GROUP_IDS
=
settings
.
COURSE_ENROLLMENT_MODES
class
EnrollmentTrackUserPartition
(
UserPartition
):
"""
Extends UserPartition to support dynamic groups pulled from the current course Enrollment tracks.
"""
@property
def
groups
(
self
):
"""
Return the groups (based on CourseModes) for the course associated with this
EnrollmentTrackUserPartition instance.
If a course is using the Verified Track Cohorting pilot feature, this method
returns an empty array regardless of registered CourseModes.
"""
course_key
=
CourseKey
.
from_string
(
self
.
parameters
[
"course_id"
])
if
is_course_using_cohort_instead
(
course_key
):
return
[]
return
[
Group
(
ENROLLMENT_GROUP_IDS
[
mode
.
slug
],
unicode
(
mode
.
name
))
for
mode
in
CourseMode
.
modes_for_course
(
course_key
,
include_expired
=
True
,
only_selectable
=
False
)
]
def
to_json
(
self
):
"""
Because this partition is dynamic, to_json and from_json are not supported.
Calling this method will raise a TypeError.
"""
raise
TypeError
(
"Because EnrollmentTrackUserPartition is a dynamic partition, 'to_json' is not supported."
)
def
from_json
(
self
):
"""
Because this partition is dynamic, to_json and from_json are not supported.
Calling this method will raise a TypeError.
"""
raise
TypeError
(
"Because EnrollmentTrackUserPartition is a dynamic partition, 'from_json' is not supported."
)
class
EnrollmentTrackPartitionScheme
(
object
):
"""
This scheme uses learner enrollment tracks to map learners into partition groups.
"""
@classmethod
def
get_group_for_user
(
cls
,
course_key
,
user
,
user_partition
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Returns the Group from the specified user partition to which the user
is assigned, via enrollment mode.
If a course is using the Verified Track Cohorting pilot feature, this method
returns None regardless of the user's enrollment mode.
"""
if
is_course_using_cohort_instead
(
course_key
):
return
None
# NOTE: masquerade code was copied from CohortPartitionScheme, and it may need
# some changes (or if not, code should be refactored out and shared).
# This work will be done in a future story TNL-6739.
# First, check if we have to deal with masquerading.
# If the current user is masquerading as a specific student, use the
# same logic as normal to return that student's group. If the current
# user is masquerading as a generic student in a specific group, then
# return that group.
if
get_course_masquerade
(
user
,
course_key
)
and
not
is_masquerading_as_specific_student
(
user
,
course_key
):
group_id
,
user_partition_id
=
get_masquerading_group_info
(
user
,
course_key
)
if
user_partition_id
==
user_partition
.
id
and
group_id
is
not
None
:
try
:
return
user_partition
.
get_group
(
group_id
)
except
NoSuchUserPartitionGroupError
:
return
None
# The user is masquerading as a generic student. We can't show any particular group.
return
None
mode_slug
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
user
,
course_key
)
if
mode_slug
and
is_active
:
course_mode
=
CourseMode
.
mode_for_course
(
course_key
,
mode_slug
,
modes
=
CourseMode
.
modes_for_course
(
course_key
,
include_expired
=
True
,
only_selectable
=
False
),
)
if
not
course_mode
:
course_mode
=
CourseMode
.
DEFAULT_MODE
return
Group
(
ENROLLMENT_GROUP_IDS
[
course_mode
.
slug
],
unicode
(
course_mode
.
name
))
else
:
return
None
@classmethod
def
create_user_partition
(
cls
,
id
,
name
,
description
,
groups
=
None
,
parameters
=
None
,
active
=
True
):
# pylint: disable=redefined-builtin, invalid-name, unused-argument
"""
Create a custom UserPartition to support dynamic groups.
A Partition has an id, name, scheme, description, parameters, and a list
of groups. The id is intended to be unique within the context where these
are used. (e.g., for partitions of users within a course, the ids should
be unique per-course). The scheme is used to assign users into groups.
The parameters field is used to save extra parameters e.g., location of
the course ID for this partition scheme.
Partitions can be marked as inactive by setting the "active" flag to False.
Any group access rule referencing inactive partitions will be ignored
when performing access checks.
"""
return
EnrollmentTrackUserPartition
(
id
,
name
,
description
,
[],
cls
,
parameters
,
active
)
def
is_course_using_cohort_instead
(
course_key
):
"""
Returns whether the given course_context is using verified-track cohorts
and therefore shouldn't use a track-based partition.
"""
return
VerifiedTrackCohortedCourse
.
is_verified_track_cohort_enabled
(
course_key
)
lms
/djangoapps/verified_track_content/tasks.py
→
openedx/core
/djangoapps/verified_track_content/tasks.py
View file @
e5035746
File moved
lms
/djangoapps/verified_track_content/tests/__init__.py
→
openedx/core
/djangoapps/verified_track_content/tests/__init__.py
View file @
e5035746
File moved
lms
/djangoapps/verified_track_content/tests/test_forms.py
→
openedx/core
/djangoapps/verified_track_content/tests/test_forms.py
View file @
e5035746
...
...
@@ -4,7 +4,7 @@ Test for forms helpers.
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
verified_track_content.forms
import
VerifiedTrackCourseForm
from
openedx.core.djangoapps.
verified_track_content.forms
import
VerifiedTrackCourseForm
class
TestVerifiedTrackCourseForm
(
SharedModuleStoreTestCase
):
...
...
lms
/djangoapps/verified_track_content/tests/test_models.py
→
openedx/core
/djangoapps/verified_track_content/tests/test_models.py
View file @
e5035746
"""
Tests for Verified Track Cohorting models
"""
# pylint: disable=attribute-defined-outside-init
# pylint: disable=no-member
from
django.test
import
TestCase
import
mock
from
mock
import
patch
...
...
@@ -12,11 +15,12 @@ from student.models import CourseMode
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
verified_track_content
.models
import
VerifiedTrackCohortedCourse
,
DEFAULT_VERIFIED_COHORT_NAME
from
verified_track_content
.tasks
import
sync_cohort_with_mode
from
.
.models
import
VerifiedTrackCohortedCourse
,
DEFAULT_VERIFIED_COHORT_NAME
from
.
.tasks
import
sync_cohort_with_mode
from
openedx.core.djangoapps.course_groups.cohorts
import
(
set_course_cohort_settings
,
add_cohort
,
CourseCohort
,
DEFAULT_COHORT_NAME
)
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
class
TestVerifiedTrackCohortedCourse
(
TestCase
):
...
...
@@ -48,13 +52,13 @@ class TestVerifiedTrackCohortedCourse(TestCase):
self
.
assertEqual
(
unicode
(
config
),
"Course: {}, enabled: True"
.
format
(
self
.
SAMPLE_COURSE
))
def
test_verified_cohort_name
(
self
):
COHORT_NAME
=
'verified cohort'
cohort_name
=
'verified cohort'
course_key
=
CourseKey
.
from_string
(
self
.
SAMPLE_COURSE
)
config
=
VerifiedTrackCohortedCourse
.
objects
.
create
(
course_key
=
course_key
,
enabled
=
True
,
verified_cohort_name
=
COHORT_NAME
course_key
=
course_key
,
enabled
=
True
,
verified_cohort_name
=
cohort_name
)
config
.
save
()
self
.
assertEqual
(
VerifiedTrackCohortedCourse
.
verified_cohort_name_for_course
(
course_key
),
COHORT_NAME
)
self
.
assertEqual
(
VerifiedTrackCohortedCourse
.
verified_cohort_name_for_course
(
course_key
),
cohort_name
)
def
test_unset_verified_cohort_name
(
self
):
fake_course_id
=
'fake/course/key'
...
...
@@ -62,6 +66,7 @@ class TestVerifiedTrackCohortedCourse(TestCase):
self
.
assertEqual
(
VerifiedTrackCohortedCourse
.
verified_cohort_name_for_course
(
course_key
),
None
)
@skip_unless_lms
class
TestMoveToVerified
(
SharedModuleStoreTestCase
):
""" Tests for the post-save listener. """
...
...
@@ -82,12 +87,15 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
self
.
addCleanup
(
celery_task_patcher
.
stop
)
def
_enable_cohorting
(
self
):
""" Turn on cohorting in the course. """
set_course_cohort_settings
(
self
.
course
.
id
,
is_cohorted
=
True
)
def
_create_verified_cohort
(
self
,
name
=
DEFAULT_VERIFIED_COHORT_NAME
):
""" Create a verified cohort. """
add_cohort
(
self
.
course
.
id
,
name
,
CourseCohort
.
MANUAL
)
def
_create_named_random_cohort
(
self
,
name
):
""" Create a random cohort with the supplied name. """
return
add_cohort
(
self
.
course
.
id
,
name
,
CourseCohort
.
RANDOM
)
def
_enable_verified_track_cohorting
(
self
,
cohort_name
=
None
):
...
...
@@ -101,6 +109,7 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
config
.
save
()
def
_enroll_in_course
(
self
):
""" Enroll self.user in self.course. """
self
.
enrollment
=
CourseEnrollmentFactory
(
course_id
=
self
.
course
.
id
,
user
=
self
.
user
)
def
_upgrade_to_verified
(
self
):
...
...
@@ -108,6 +117,7 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
self
.
enrollment
.
update_enrollment
(
mode
=
CourseMode
.
VERIFIED
)
def
_verify_no_automatic_cohorting
(
self
):
""" Check that upgrading self.user to verified does not move them into a cohort. """
self
.
_enroll_in_course
()
self
.
assertIsNone
(
get_cohort
(
self
.
user
,
self
.
course
.
id
,
assign
=
False
))
self
.
_upgrade_to_verified
()
...
...
@@ -115,13 +125,15 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
self
.
assertEqual
(
0
,
self
.
mocked_celery_task
.
call_count
)
def
_unenroll
(
self
):
""" Unenroll self.user from self.course. """
self
.
enrollment
.
unenroll
(
self
.
user
,
self
.
course
.
id
)
def
_reenroll
(
self
):
""" Re-enroll the learner into mode AUDIT. """
self
.
enrollment
.
activate
()
self
.
enrollment
.
change_mode
(
CourseMode
.
AUDIT
)
@mock.patch
(
'verified_track_content.models.log.error'
)
@mock.patch
(
'
openedx.core.djangoapps.
verified_track_content.models.log.error'
)
def
test_automatic_cohorting_disabled
(
self
,
error_logger
):
"""
If the VerifiedTrackCohortedCourse feature is disabled for a course, enrollment mode changes do not move
...
...
@@ -136,7 +148,7 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
# No logging occurs if feature is disabled for course.
self
.
assertFalse
(
error_logger
.
called
)
@mock.patch
(
'verified_track_content.models.log.error'
)
@mock.patch
(
'
openedx.core.djangoapps.
verified_track_content.models.log.error'
)
def
test_cohorting_enabled_course_not_cohorted
(
self
,
error_logger
):
"""
If the VerifiedTrackCohortedCourse feature is enabled for a course, but the course is not cohorted,
...
...
@@ -149,7 +161,7 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
self
.
assertTrue
(
error_logger
.
called
)
self
.
assertIn
(
"course is not cohorted"
,
error_logger
.
call_args
[
0
][
0
])
@mock.patch
(
'verified_track_content.models.log.error'
)
@mock.patch
(
'
openedx.core.djangoapps.
verified_track_content.models.log.error'
)
def
test_cohorting_enabled_missing_verified_cohort
(
self
,
error_logger
):
"""
If the VerifiedTrackCohortedCourse feature is enabled for a course and the course is cohorted,
...
...
openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py
0 → 100644
View file @
e5035746
"""
Tests for verified_track_content/partition_scheme.py.
"""
from
datetime
import
datetime
,
timedelta
import
pytz
from
..partition_scheme
import
EnrollmentTrackPartitionScheme
,
EnrollmentTrackUserPartition
,
ENROLLMENT_GROUP_IDS
from
..models
import
VerifiedTrackCohortedCourse
from
course_modes.models
import
CourseMode
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
class
EnrollmentTrackUserPartitionTest
(
SharedModuleStoreTestCase
):
"""
Tests for the custom EnrollmentTrackUserPartition (dynamic groups).
"""
@classmethod
def
setUpClass
(
cls
):
super
(
EnrollmentTrackUserPartitionTest
,
cls
)
.
setUpClass
()
cls
.
course
=
CourseFactory
.
create
()
def
test_only_default_mode
(
self
):
partition
=
create_enrollment_track_partition
(
self
.
course
)
groups
=
partition
.
groups
self
.
assertEqual
(
1
,
len
(
groups
))
self
.
assertEqual
(
"Audit"
,
groups
[
0
]
.
name
)
def
test_using_verified_track_cohort
(
self
):
VerifiedTrackCohortedCourse
.
objects
.
create
(
course_key
=
self
.
course
.
id
,
enabled
=
True
)
.
save
()
partition
=
create_enrollment_track_partition
(
self
.
course
)
self
.
assertEqual
(
0
,
len
(
partition
.
groups
))
def
test_multiple_groups
(
self
):
create_mode
(
self
.
course
,
CourseMode
.
AUDIT
,
"Audit Enrollment Track"
,
min_price
=
0
)
# Note that the verified mode is expired-- this is intentional.
create_mode
(
self
.
course
,
CourseMode
.
VERIFIED
,
"Verified Enrollment Track"
,
min_price
=
1
,
expiration_datetime
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=-
1
)
)
# Note that the credit mode is not selectable-- this is intentional.
create_mode
(
self
.
course
,
CourseMode
.
CREDIT_MODE
,
"Credit Mode"
,
min_price
=
2
)
partition
=
create_enrollment_track_partition
(
self
.
course
)
groups
=
partition
.
groups
self
.
assertEqual
(
3
,
len
(
groups
))
self
.
assertIsNotNone
(
self
.
get_group_by_name
(
partition
,
"Audit Enrollment Track"
))
self
.
assertIsNotNone
(
self
.
get_group_by_name
(
partition
,
"Verified Enrollment Track"
))
self
.
assertIsNotNone
(
self
.
get_group_by_name
(
partition
,
"Credit Mode"
))
def
test_to_json_not_supported
(
self
):
user_partition
=
create_enrollment_track_partition
(
self
.
course
)
with
self
.
assertRaises
(
TypeError
):
user_partition
.
to_json
()
def
test_from_json_not_supported
(
self
):
with
self
.
assertRaises
(
TypeError
):
EnrollmentTrackUserPartition
.
from_json
()
def
test_group_ids
(
self
):
"""
Test that group IDs are all less than MINIMUM_STATIC_PARTITION_ID (to avoid overlapping
with group IDs associated with cohort and random user partitions).
"""
for
mode
in
ENROLLMENT_GROUP_IDS
:
self
.
assertLess
(
ENROLLMENT_GROUP_IDS
[
mode
],
MINIMUM_STATIC_PARTITION_ID
)
@staticmethod
def
get_group_by_name
(
partition
,
name
):
"""
Return the group in the EnrollmentTrackUserPartition with the given name.
If no such group exists, returns `None`.
"""
for
group
in
partition
.
groups
:
if
group
.
name
==
name
:
return
group
return
None
class
EnrollmentTrackPartitionSchemeTest
(
SharedModuleStoreTestCase
):
"""
Tests for EnrollmentTrackPartitionScheme.
"""
@classmethod
def
setUpClass
(
cls
):
super
(
EnrollmentTrackPartitionSchemeTest
,
cls
)
.
setUpClass
()
cls
.
course
=
CourseFactory
.
create
()
cls
.
student
=
UserFactory
()
def
test_get_scheme
(
self
):
"""
Ensure that the scheme extension is correctly plugged in (via entry point in setup.py)
"""
self
.
assertEquals
(
UserPartition
.
get_scheme
(
'enrollment_track'
),
EnrollmentTrackPartitionScheme
)
def
test_create_user_partition
(
self
):
user_partition
=
UserPartition
.
get_scheme
(
'enrollment_track'
)
.
create_user_partition
(
301
,
"partition"
,
"test partition"
,
parameters
=
{
"course_id"
:
unicode
(
self
.
course
.
id
)}
)
self
.
assertEqual
(
type
(
user_partition
),
EnrollmentTrackUserPartition
)
self
.
assertEqual
(
user_partition
.
name
,
"partition"
)
groups
=
user_partition
.
groups
self
.
assertEqual
(
1
,
len
(
groups
))
self
.
assertEqual
(
"Audit"
,
groups
[
0
]
.
name
)
def
test_not_enrolled
(
self
):
self
.
assertIsNone
(
self
.
_get_user_group
())
def
test_default_enrollment
(
self
):
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
)
self
.
assertEqual
(
"Audit"
,
self
.
_get_user_group
()
.
name
)
def
test_enrolled_in_nonexistent_mode
(
self
):
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
,
mode
=
CourseMode
.
VERIFIED
)
self
.
assertEqual
(
"Audit"
,
self
.
_get_user_group
()
.
name
)
def
test_enrolled_in_verified
(
self
):
create_mode
(
self
.
course
,
CourseMode
.
VERIFIED
,
"Verified Enrollment Track"
,
min_price
=
1
)
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
,
mode
=
CourseMode
.
VERIFIED
)
self
.
assertEqual
(
"Verified Enrollment Track"
,
self
.
_get_user_group
()
.
name
)
def
test_enrolled_in_expired
(
self
):
create_mode
(
self
.
course
,
CourseMode
.
VERIFIED
,
"Verified Enrollment Track"
,
min_price
=
1
,
expiration_datetime
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=-
1
)
)
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
,
mode
=
CourseMode
.
VERIFIED
)
self
.
assertEqual
(
"Verified Enrollment Track"
,
self
.
_get_user_group
()
.
name
)
def
test_enrolled_in_non_selectable
(
self
):
create_mode
(
self
.
course
,
CourseMode
.
CREDIT_MODE
,
"Credit Enrollment Track"
,
min_price
=
1
)
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
,
mode
=
CourseMode
.
CREDIT_MODE
)
self
.
assertEqual
(
"Credit Enrollment Track"
,
self
.
_get_user_group
()
.
name
)
def
test_using_verified_track_cohort
(
self
):
VerifiedTrackCohortedCourse
.
objects
.
create
(
course_key
=
self
.
course
.
id
,
enabled
=
True
)
.
save
()
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
)
self
.
assertIsNone
(
self
.
_get_user_group
())
def
_get_user_group
(
self
):
"""
Gets the group the user is assigned to.
"""
user_partition
=
create_enrollment_track_partition
(
self
.
course
)
return
user_partition
.
scheme
.
get_group_for_user
(
self
.
course
.
id
,
self
.
student
,
user_partition
)
def
create_enrollment_track_partition
(
course
):
"""
Create an EnrollmentTrackUserPartition instance for the given course.
"""
enrollment_track_scheme
=
UserPartition
.
get_scheme
(
"enrollment_track"
)
partition
=
enrollment_track_scheme
.
create_user_partition
(
id
=
1
,
name
=
"TestEnrollment Track Partition"
,
description
=
"Test partition for segmenting users by enrollment track"
,
parameters
=
{
"course_id"
:
unicode
(
course
.
id
)}
)
return
partition
def
create_mode
(
course
,
mode_slug
,
mode_name
,
min_price
=
0
,
expiration_datetime
=
None
):
"""
Create a new course mode for the given course.
"""
return
CourseMode
.
objects
.
get_or_create
(
course_id
=
course
.
id
,
mode_display_name
=
mode_name
,
mode_slug
=
mode_slug
,
min_price
=
min_price
,
suggested_prices
=
''
,
_expiration_datetime
=
expiration_datetime
,
currency
=
'usd'
)
lms
/djangoapps/verified_track_content/tests/test_views.py
→
openedx/core
/djangoapps/verified_track_content/tests/test_views.py
View file @
e5035746
...
...
@@ -5,22 +5,21 @@ Tests for verified track content views.
import
json
from
nose.plugins.attrib
import
attr
from
unittest
import
skipUnless
from
django.http
import
Http404
from
django.test.client
import
RequestFactory
from
django.conf
import
settings
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
from
student.tests.factories
import
UserFactory
,
AdminFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
verified_track_content
.models
import
VerifiedTrackCohortedCourse
from
verified_track_content
.views
import
cohorting_settings
from
.
.models
import
VerifiedTrackCohortedCourse
from
.
.views
import
cohorting_settings
@attr
(
shard
=
2
)
@skip
Unless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Tests only valid in LMS'
)
@skip
_unless_lms
class
CohortingSettingsTestCase
(
SharedModuleStoreTestCase
):
"""
Tests the `cohort_discussion_topics` view.
...
...
@@ -65,6 +64,7 @@ class CohortingSettingsTestCase(SharedModuleStoreTestCase):
self
.
_verify_cohort_settings_response
(
expected_response
)
def
_verify_cohort_settings_response
(
self
,
expected_response
):
""" Verify that the response was successful and matches the expected JSON payload. """
request
=
RequestFactory
()
.
get
(
"dummy_url"
)
request
.
user
=
AdminFactory
()
response
=
cohorting_settings
(
request
,
unicode
(
self
.
course
.
id
))
...
...
lms
/djangoapps/verified_track_content/views.py
→
openedx/core
/djangoapps/verified_track_content/views.py
View file @
e5035746
...
...
@@ -6,9 +6,9 @@ from util.json_request import expect_json, JsonResponse
from
django.contrib.auth.decorators
import
login_required
from
opaque_keys.edx.keys
import
CourseKey
from
courseware.courses
import
get_course_with_access
from
lms.djangoapps.
courseware.courses
import
get_course_with_access
from
verified_track_content.models
import
VerifiedTrackCohortedCourse
from
openedx.core.djangoapps.
verified_track_content.models
import
VerifiedTrackCohortedCourse
@expect_json
...
...
setup.py
View file @
e5035746
...
...
@@ -42,6 +42,7 @@ setup(
"random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme"
,
"cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme"
,
"verification = openedx.core.djangoapps.credit.partition_schemes:VerificationPartitionScheme"
,
"enrollment_track = openedx.core.djangoapps.verified_track_content.partition_scheme:EnrollmentTrackPartitionScheme"
,
],
"openedx.block_structure_transformer"
:
[
"library_content = lms.djangoapps.course_blocks.transformers.library_content:ContentLibraryTransformer"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment