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
6fb3daa7
Commit
6fb3daa7
authored
Dec 15, 2014
by
jsa
Committed by
Andy Armstrong
Jan 15, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
support group_access. JIRA: TNL-649
parent
17d892c5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
352 additions
and
57 deletions
+352
-57
common/lib/xmodule/xmodule/modulestore/inheritance.py
+0
-9
common/lib/xmodule/xmodule/modulestore/mongo/base.py
+0
-5
common/lib/xmodule/xmodule/partitions/partitions.py
+23
-4
common/lib/xmodule/xmodule/partitions/tests/test_partitions.py
+20
-1
lms/djangoapps/courseware/access.py
+72
-0
lms/djangoapps/courseware/tests/test_access.py
+2
-2
lms/djangoapps/courseware/tests/test_group_access.py
+0
-0
lms/djangoapps/lms_xblock/mixin.py
+69
-28
lms/lib/xblock/test/test_mixin.py
+139
-0
openedx/core/djangoapps/course_groups/partition_scheme.py
+7
-5
openedx/core/djangoapps/user_api/partition_schemes.py
+20
-3
No files found.
common/lib/xmodule/xmodule/modulestore/inheritance.py
View file @
6fb3daa7
...
@@ -57,15 +57,6 @@ class InheritanceMixin(XBlockMixin):
...
@@ -57,15 +57,6 @@ class InheritanceMixin(XBlockMixin):
default
=
False
,
default
=
False
,
scope
=
Scope
.
settings
,
scope
=
Scope
.
settings
,
)
)
group_access
=
Dict
(
help
=
"A dictionary that maps which groups can be shown this block. The keys "
"are group configuration ids and the values are a list of group IDs. "
"If there is no key for a group configuration or if the list of group IDs "
"is empty then the block is considered visible to all. Note that this "
"field is ignored if the block is visible_to_staff_only."
,
default
=
{},
scope
=
Scope
.
settings
,
)
course_edit_method
=
String
(
course_edit_method
=
String
(
display_name
=
_
(
"Course Editor"
),
display_name
=
_
(
"Course Editor"
),
help
=
_
(
"Enter the method by which this course is edited (
\"
XML
\"
or
\"
Studio
\"
)."
),
help
=
_
(
"Enter the method by which this course is edited (
\"
XML
\"
or
\"
Studio
\"
)."
),
...
...
common/lib/xmodule/xmodule/modulestore/mongo/base.py
View file @
6fb3daa7
...
@@ -475,11 +475,6 @@ class ParentLocationCache(dict):
...
@@ -475,11 +475,6 @@ class ParentLocationCache(dict):
def
set
(
self
,
key
,
value
):
def
set
(
self
,
key
,
value
):
self
[
key
]
=
value
self
[
key
]
=
value
@contract
(
key
=
unicode
)
def
delete
(
self
,
key
):
if
key
in
self
:
del
self
[
key
]
@contract
(
value
=
"BlockUsageLocator"
)
@contract
(
value
=
"BlockUsageLocator"
)
def
delete_by_value
(
self
,
value
):
def
delete_by_value
(
self
,
value
):
keys_to_delete
=
[
k
for
k
,
v
in
self
.
iteritems
()
if
v
==
value
]
keys_to_delete
=
[
k
for
k
,
v
in
self
.
iteritems
()
if
v
==
value
]
...
...
common/lib/xmodule/xmodule/partitions/partitions.py
View file @
6fb3daa7
...
@@ -10,7 +10,21 @@ from stevedore.extension import ExtensionManager
...
@@ -10,7 +10,21 @@ from stevedore.extension import ExtensionManager
class
UserPartitionError
(
Exception
):
class
UserPartitionError
(
Exception
):
"""
"""
An error was found regarding user partitions.
Base Exception for when an error was found regarding user partitions.
"""
pass
class
NoSuchUserPartitionError
(
UserPartitionError
):
"""
Exception to be raised when looking up a UserPartition by its ID fails.
"""
pass
class
NoSuchUserPartitionGroupError
(
UserPartitionError
):
"""
Exception to be raised when looking up a UserPartition Group by its ID fails.
"""
"""
pass
pass
...
@@ -171,9 +185,14 @@ class UserPartition(namedtuple("UserPartition", "id name description groups sche
...
@@ -171,9 +185,14 @@ class UserPartition(namedtuple("UserPartition", "id name description groups sche
def
get_group
(
self
,
group_id
):
def
get_group
(
self
,
group_id
):
"""
"""
Returns the group with the specified id.
Returns the group with the specified id.
Raises NoSuchUserPartitionGroupError if not found.
"""
"""
for
group
in
self
.
groups
:
# pylint: disable=no-member
# pylint: disable=no-member
for
group
in
self
.
groups
:
if
group
.
id
==
group_id
:
if
group
.
id
==
group_id
:
return
group
return
group
return
None
raise
NoSuchUserPartitionGroupError
(
"could not find a Group with ID [{}] in UserPartition [{}]"
.
format
(
group_id
,
self
.
id
)
)
common/lib/xmodule/xmodule/partitions/tests/test_partitions.py
View file @
6fb3daa7
...
@@ -8,7 +8,9 @@ from mock import Mock
...
@@ -8,7 +8,9 @@ from mock import Mock
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
stevedore.extension
import
Extension
,
ExtensionManager
from
stevedore.extension
import
Extension
,
ExtensionManager
from
xmodule.partitions.partitions
import
Group
,
UserPartition
,
UserPartitionError
,
USER_PARTITION_SCHEME_NAMESPACE
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
...
@@ -259,6 +261,23 @@ class TestUserPartition(PartitionTestCase):
...
@@ -259,6 +261,23 @@ class TestUserPartition(PartitionTestCase):
user_partition
=
UserPartition
.
from_json
(
jsonified
)
user_partition
=
UserPartition
.
from_json
(
jsonified
)
self
.
assertNotIn
(
"programmer"
,
user_partition
.
to_json
())
self
.
assertNotIn
(
"programmer"
,
user_partition
.
to_json
())
def
test_get_group
(
self
):
"""
UserPartition.get_group correctly returns the group referenced by the
`group_id` parameter, or raises NoSuchUserPartitionGroupError when
the lookup fails.
"""
self
.
assertEqual
(
self
.
user_partition
.
get_group
(
self
.
TEST_GROUPS
[
0
]
.
id
),
# pylint: disable=no-member
self
.
TEST_GROUPS
[
0
]
)
self
.
assertEqual
(
self
.
user_partition
.
get_group
(
self
.
TEST_GROUPS
[
1
]
.
id
),
# pylint: disable=no-member
self
.
TEST_GROUPS
[
1
]
)
with
self
.
assertRaises
(
NoSuchUserPartitionGroupError
):
self
.
user_partition
.
get_group
(
3
)
class
StaticPartitionService
(
PartitionService
):
class
StaticPartitionService
(
PartitionService
):
"""
"""
...
...
lms/djangoapps/courseware/access.py
View file @
6fb3daa7
...
@@ -15,6 +15,7 @@ from xmodule.error_module import ErrorDescriptor
...
@@ -15,6 +15,7 @@ from xmodule.error_module import ErrorDescriptor
from
xmodule.x_module
import
XModule
from
xmodule.x_module
import
XModule
from
xblock.core
import
XBlock
from
xblock.core
import
XBlock
from
xmodule.partitions.partitions
import
NoSuchUserPartitionError
,
NoSuchUserPartitionGroupError
from
external_auth.models
import
ExternalAuthMap
from
external_auth.models
import
ExternalAuthMap
from
courseware.masquerade
import
is_masquerading_as_student
from
courseware.masquerade
import
is_masquerading_as_student
...
@@ -301,6 +302,71 @@ def _has_access_error_desc(user, action, descriptor, course_key):
...
@@ -301,6 +302,71 @@ def _has_access_error_desc(user, action, descriptor, course_key):
return
_dispatch
(
checkers
,
action
,
user
,
descriptor
)
return
_dispatch
(
checkers
,
action
,
user
,
descriptor
)
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
)
==
0
:
# short circuit the process, since there are no defined user partitions
return
True
# use merged_group_access which takes group access on the block's
# parents / ancestors into account
merged_access
=
descriptor
.
merged_group_access
# check for False in merged_access, which indicates that at least one
# partition's group list excludes all students.
if
False
in
merged_access
.
values
():
log
.
warning
(
"Group access check excludes all students, access will be denied."
,
exc_info
=
True
)
return
False
# resolve the partition IDs in group_access to actual
# partition objects, skipping those which contain empty group directives.
# if a referenced partition could not be found, access will be denied.
try
:
partitions
=
[
descriptor
.
_get_user_partition
(
partition_id
)
# pylint:disable=protected-access
for
partition_id
,
group_ids
in
merged_access
.
items
()
if
group_ids
is
not
None
]
except
NoSuchUserPartitionError
:
log
.
warning
(
"Error looking up user partition, access will be denied."
,
exc_info
=
True
)
return
False
# next resolve the group IDs specified within each partition
partition_groups
=
[]
try
:
for
partition
in
partitions
:
groups
=
[
partition
.
get_group
(
group_id
)
for
group_id
in
merged_access
[
partition
.
id
]
]
if
groups
:
partition_groups
.
append
((
partition
,
groups
))
except
NoSuchUserPartitionGroupError
:
log
.
warning
(
"Error looking up referenced user partition group, access will be denied."
,
exc_info
=
True
)
return
False
# look up the user's group for each partition
user_groups
=
{}
for
partition
,
groups
in
partition_groups
:
user_groups
[
partition
.
id
]
=
partition
.
scheme
.
get_group_for_user
(
course_key
,
user
,
partition
,
)
# finally: check that the user has a satisfactory group assignment
# for each partition.
if
not
all
(
user_groups
.
get
(
partition
.
id
)
in
groups
for
partition
,
groups
in
partition_groups
):
return
False
# all checks passed.
return
True
def
_has_access_descriptor
(
user
,
action
,
descriptor
,
course_key
=
None
):
def
_has_access_descriptor
(
user
,
action
,
descriptor
,
course_key
=
None
):
"""
"""
Check if user has access to this descriptor.
Check if user has access to this descriptor.
...
@@ -323,6 +389,12 @@ def _has_access_descriptor(user, action, descriptor, course_key=None):
...
@@ -323,6 +389,12 @@ def _has_access_descriptor(user, action, descriptor, course_key=None):
if
descriptor
.
visible_to_staff_only
and
not
_has_staff_access_to_descriptor
(
user
,
descriptor
,
course_key
):
if
descriptor
.
visible_to_staff_only
and
not
_has_staff_access_to_descriptor
(
user
,
descriptor
,
course_key
):
return
False
return
False
# enforce group access
if
not
_has_group_access
(
descriptor
,
user
,
course_key
):
# if group_access check failed, deny access unless the requestor is staff,
# in which case immediately grant access.
return
_has_staff_access_to_descriptor
(
user
,
descriptor
,
course_key
)
# If start dates are off, can always load
# If start dates are off, can always load
if
settings
.
FEATURES
[
'DISABLE_START_DATES'
]
and
not
is_masquerading_as_student
(
user
):
if
settings
.
FEATURES
[
'DISABLE_START_DATES'
]
and
not
is_masquerading_as_student
(
user
):
debug
(
"Allow: DISABLE_START_DATES"
)
debug
(
"Allow: DISABLE_START_DATES"
)
...
...
lms/djangoapps/courseware/tests/test_access.py
View file @
6fb3daa7
...
@@ -105,7 +105,7 @@ class AccessTestCase(TestCase):
...
@@ -105,7 +105,7 @@ class AccessTestCase(TestCase):
def
test__has_access_descriptor
(
self
):
def
test__has_access_descriptor
(
self
):
# TODO: override DISABLE_START_DATES and test the start date branch of the method
# TODO: override DISABLE_START_DATES and test the start date branch of the method
user
=
Mock
()
user
=
Mock
()
descriptor
=
Mock
()
descriptor
=
Mock
(
user_partitions
=
[]
)
# Always returns true because DISABLE_START_DATES is set in test.py
# Always returns true because DISABLE_START_DATES is set in test.py
self
.
assertTrue
(
access
.
_has_access_descriptor
(
user
,
'load'
,
descriptor
))
self
.
assertTrue
(
access
.
_has_access_descriptor
(
user
,
'load'
,
descriptor
))
...
@@ -118,7 +118,7 @@ class AccessTestCase(TestCase):
...
@@ -118,7 +118,7 @@ class AccessTestCase(TestCase):
"""
"""
Tests that "visible_to_staff_only" overrides start date.
Tests that "visible_to_staff_only" overrides start date.
"""
"""
mock_unit
=
Mock
()
mock_unit
=
Mock
(
user_partitions
=
[]
)
mock_unit
.
_class_tags
=
{}
# Needed for detached check in _has_access_descriptor
mock_unit
.
_class_tags
=
{}
# Needed for detached check in _has_access_descriptor
def
verify_access
(
student_should_have_access
):
def
verify_access
(
student_should_have_access
):
...
...
lms/djangoapps/courseware/tests/test_group_access.py
0 → 100644
View file @
6fb3daa7
This diff is collapsed.
Click to expand it.
lms/djangoapps/lms_xblock/mixin.py
View file @
6fb3daa7
"""
"""
Namespace that defines fields common to all blocks used in the LMS
Namespace that defines fields common to all blocks used in the LMS
"""
"""
from
lazy
import
lazy
from
xblock.fields
import
Boolean
,
Scope
,
String
,
XBlockMixin
,
Dict
from
xblock.fields
import
Boolean
,
Scope
,
String
,
XBlockMixin
,
Dict
from
xblock.validation
import
ValidationMessage
from
xblock.validation
import
ValidationMessage
from
xmodule.modulestore.inheritance
import
UserPartitionList
from
xmodule.modulestore.inheritance
import
UserPartitionList
from
xmodule.partitions.partitions
import
NoSuchUserPartitionError
,
NoSuchUserPartitionGroupError
# Make '_' a no-op so we can scrape strings
# Make '_' a no-op so we can scrape strings
_
=
lambda
text
:
text
_
=
lambda
text
:
text
class
GroupAccessDict
(
Dict
):
"""Special Dict class for serializing the group_access field"""
def
from_json
(
self
,
access_dict
):
if
access_dict
is
not
None
:
return
{
int
(
k
):
access_dict
[
k
]
for
k
in
access_dict
}
def
to_json
(
self
,
access_dict
):
if
access_dict
is
not
None
:
return
{
unicode
(
k
):
access_dict
[
k
]
for
k
in
access_dict
}
class
LmsBlockMixin
(
XBlockMixin
):
class
LmsBlockMixin
(
XBlockMixin
):
"""
"""
Mixin that defines fields common to all blocks used in the LMS
Mixin that defines fields common to all blocks used in the LMS
...
@@ -55,16 +69,56 @@ class LmsBlockMixin(XBlockMixin):
...
@@ -55,16 +69,56 @@ class LmsBlockMixin(XBlockMixin):
default
=
False
,
default
=
False
,
scope
=
Scope
.
settings
,
scope
=
Scope
.
settings
,
)
)
group_access
=
Dict
(
help
=
"A dictionary that maps which groups can be shown this block. The keys "
group_access
=
GroupAccessDict
(
"are group configuration ids and the values are a list of group IDs. "
help
=
_
(
"If there is no key for a group configuration or if the list of group IDs "
"A dictionary that maps which groups can be shown this block. The keys "
"is empty then the block is considered visible to all. Note that this "
"are group configuration ids and the values are a list of group IDs. "
"field is ignored if the block is visible_to_staff_only."
,
"If there is no key for a group configuration or if the set of group IDs "
"is empty then the block is considered visible to all. Note that this "
"field is ignored if the block is visible_to_staff_only."
),
default
=
{},
default
=
{},
scope
=
Scope
.
settings
,
scope
=
Scope
.
settings
,
)
)
@lazy
def
merged_group_access
(
self
):
"""
This computes access to a block's group_access rules in the context of its position
within the courseware structure, in the form of a lazily-computed attribute.
Each block's group_access rule is merged recursively with its parent's, guaranteeing
that any rule in a parent block will be enforced on descendants, even if a descendant
also defined its own access rules. The return value is always a dict, with the same
structure as that of the group_access field.
When merging access rules results in a case where all groups are denied access in a
user partition (which effectively denies access to that block for all students),
the special value False will be returned for that user partition key.
"""
parent
=
self
.
get_parent
()
if
not
parent
:
return
self
.
group_access
or
{}
merged_access
=
parent
.
merged_group_access
.
copy
()
if
self
.
group_access
is
not
None
:
for
partition_id
,
group_ids
in
self
.
group_access
.
items
():
if
group_ids
:
# skip if the "local" group_access for this partition is None or empty.
if
partition_id
in
merged_access
:
if
merged_access
[
partition_id
]
is
False
:
# special case - means somewhere up the hierarchy, merged access rules have eliminated
# all group_ids from this partition, so there's no possible intersection.
continue
# otherwise, if the parent defines group access rules for this partition,
# intersect with the local ones.
merged_access
[
partition_id
]
=
list
(
set
(
merged_access
[
partition_id
])
.
intersection
(
group_ids
)
)
or
False
else
:
# add the group access rules for this partition to the merged set of rules.
merged_access
[
partition_id
]
=
group_ids
return
merged_access
# Specified here so we can see what the value set at the course-level is.
# Specified here so we can see what the value set at the course-level is.
user_partitions
=
UserPartitionList
(
user_partitions
=
UserPartitionList
(
help
=
_
(
"The list of group configurations for partitioning students in content experiments."
),
help
=
_
(
"The list of group configurations for partitioning students in content experiments."
),
...
@@ -74,29 +128,14 @@ class LmsBlockMixin(XBlockMixin):
...
@@ -74,29 +128,14 @@ class LmsBlockMixin(XBlockMixin):
def
_get_user_partition
(
self
,
user_partition_id
):
def
_get_user_partition
(
self
,
user_partition_id
):
"""
"""
Returns the user partition with the specified id, or None if there is no such partition.
Returns the user partition with the specified id. Raises
`NoSuchUserPartitionError` if the lookup fails.
"""
"""
for
user_partition
in
self
.
user_partitions
:
for
user_partition
in
self
.
user_partitions
:
if
user_partition
.
id
==
user_partition_id
:
if
user_partition
.
id
==
user_partition_id
:
return
user_partition
return
user_partition
return
None
raise
NoSuchUserPartitionError
(
"could not find a UserPartition with ID [{}]"
.
format
(
user_partition_id
))
def
is_visible_to_group
(
self
,
user_partition
,
group
):
"""
Returns true if this xblock should be shown to a user in the specified user partition group.
This method returns true if one of the following is true:
- the xblock has no group_access dictionary specified
- if the dictionary has no key for the user partition's id
- if the value for the user partition's id is an empty list
- if the value for the user partition's id contains the specified group's id
"""
if
not
self
.
group_access
:
return
True
group_ids
=
self
.
group_access
.
get
(
user_partition
.
id
,
[])
if
len
(
group_ids
)
==
0
:
return
True
return
group
.
id
in
group_ids
def
validate
(
self
):
def
validate
(
self
):
"""
"""
...
@@ -105,8 +144,9 @@ class LmsBlockMixin(XBlockMixin):
...
@@ -105,8 +144,9 @@ class LmsBlockMixin(XBlockMixin):
_
=
self
.
runtime
.
service
(
self
,
"i18n"
)
.
ugettext
# pylint: disable=redefined-outer-name
_
=
self
.
runtime
.
service
(
self
,
"i18n"
)
.
ugettext
# pylint: disable=redefined-outer-name
validation
=
super
(
LmsBlockMixin
,
self
)
.
validate
()
validation
=
super
(
LmsBlockMixin
,
self
)
.
validate
()
for
user_partition_id
,
group_ids
in
self
.
group_access
.
iteritems
():
for
user_partition_id
,
group_ids
in
self
.
group_access
.
iteritems
():
user_partition
=
self
.
_get_user_partition
(
user_partition_id
)
try
:
if
not
user_partition
:
user_partition
=
self
.
_get_user_partition
(
user_partition_id
)
except
NoSuchUserPartitionError
:
validation
.
add
(
validation
.
add
(
ValidationMessage
(
ValidationMessage
(
ValidationMessage
.
ERROR
,
ValidationMessage
.
ERROR
,
...
@@ -115,8 +155,9 @@ class LmsBlockMixin(XBlockMixin):
...
@@ -115,8 +155,9 @@ class LmsBlockMixin(XBlockMixin):
)
)
else
:
else
:
for
group_id
in
group_ids
:
for
group_id
in
group_ids
:
group
=
user_partition
.
get_group
(
group_id
)
try
:
if
not
group
:
user_partition
.
get_group
(
group_id
)
except
NoSuchUserPartitionGroupError
:
validation
.
add
(
validation
.
add
(
ValidationMessage
(
ValidationMessage
(
ValidationMessage
.
ERROR
,
ValidationMessage
.
ERROR
,
...
...
lms/lib/xblock/test/test_mixin.py
View file @
6fb3daa7
...
@@ -242,3 +242,142 @@ class XBlockGetParentTest(LmsXBlockMixinTestCase):
...
@@ -242,3 +242,142 @@ class XBlockGetParentTest(LmsXBlockMixinTestCase):
old_parent_location
,
old_parent_location
,
video
.
get_parent
()
.
location
.
for_branch
(
None
)
video
.
get_parent
()
.
location
.
for_branch
(
None
)
)
)
class
RenamedTuple
(
tuple
):
# pylint: disable=incomplete-protocol
"""
This class is only used to allow overriding __name__ on the tuples passed
through ddt, in order to have the generated test names make sense.
"""
pass
def
ddt_named
(
parent
,
child
):
"""
Helper to get more readable dynamically-generated test names from ddt.
"""
args
=
RenamedTuple
([
parent
,
child
])
setattr
(
args
,
'__name__'
,
'parent_{}_child_{}'
.
format
(
parent
,
child
))
return
args
@ddt.ddt
class
XBlockMergedGroupAccessTest
(
LmsXBlockMixinTestCase
):
"""
Test that XBlock.merged_group_access is computed correctly according to
our access control rules.
"""
PARTITION_1
=
1
PARTITION_1_GROUP_1
=
11
PARTITION_1_GROUP_2
=
12
PARTITION_2
=
2
PARTITION_2_GROUP_1
=
21
PARTITION_2_GROUP_2
=
22
PARENT_CHILD_PAIRS
=
(
ddt_named
(
'section'
,
'subsection'
),
ddt_named
(
'section'
,
'vertical'
),
ddt_named
(
'section'
,
'video'
),
ddt_named
(
'subsection'
,
'vertical'
),
ddt_named
(
'subsection'
,
'video'
),
)
def
setUp
(
self
):
super
(
XBlockMergedGroupAccessTest
,
self
)
.
setUp
()
self
.
build_course
()
def
set_group_access
(
self
,
block
,
access_dict
):
"""
DRY helper.
"""
block
.
group_access
=
access_dict
block
.
runtime
.
modulestore
.
update_item
(
block
,
1
)
@ddt.data
(
*
PARENT_CHILD_PAIRS
)
@ddt.unpack
def
test_intersecting_groups
(
self
,
parent
,
child
):
"""
When merging group_access on a block, the resulting group IDs for each
partition is the intersection of the group IDs defined for that
partition across all ancestor blocks (including this one).
"""
parent_block
=
getattr
(
self
,
parent
)
child_block
=
getattr
(
self
,
child
)
self
.
set_group_access
(
parent_block
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
,
self
.
PARTITION_1_GROUP_2
]})
self
.
set_group_access
(
child_block
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_2
]})
self
.
assertEqual
(
parent_block
.
merged_group_access
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
,
self
.
PARTITION_1_GROUP_2
]},
)
self
.
assertEqual
(
child_block
.
merged_group_access
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_2
]},
)
@ddt.data
(
*
PARENT_CHILD_PAIRS
)
@ddt.unpack
def
test_disjoint_groups
(
self
,
parent
,
child
):
"""
When merging group_access on a block, if the intersection of group IDs
for a partition is empty, the merged value for that partition is False.
"""
parent_block
=
getattr
(
self
,
parent
)
child_block
=
getattr
(
self
,
child
)
self
.
set_group_access
(
parent_block
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
]})
self
.
set_group_access
(
child_block
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_2
]})
self
.
assertEqual
(
parent_block
.
merged_group_access
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
]},
)
self
.
assertEqual
(
child_block
.
merged_group_access
,
{
self
.
PARTITION_1
:
False
},
)
def
test_disjoint_groups_no_override
(
self
):
"""
Special case of the above test - ensures that `False` propagates down
to the block being queried even if blocks further down in the hierarchy
try to override it.
"""
self
.
set_group_access
(
self
.
section
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
]})
self
.
set_group_access
(
self
.
subsection
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_2
]})
self
.
set_group_access
(
self
.
vertical
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
,
self
.
PARTITION_1_GROUP_2
]})
self
.
assertEqual
(
self
.
vertical
.
merged_group_access
,
{
self
.
PARTITION_1
:
False
},
)
self
.
assertEqual
(
self
.
video
.
merged_group_access
,
{
self
.
PARTITION_1
:
False
},
)
@ddt.data
(
*
PARENT_CHILD_PAIRS
)
@ddt.unpack
def
test_union_partitions
(
self
,
parent
,
child
):
"""
When merging group_access on a block, the result's keys (partitions)
are the union of all partitions specified across all ancestor blocks
(including this one).
"""
parent_block
=
getattr
(
self
,
parent
)
child_block
=
getattr
(
self
,
child
)
self
.
set_group_access
(
parent_block
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
]})
self
.
set_group_access
(
child_block
,
{
self
.
PARTITION_2
:
[
self
.
PARTITION_1_GROUP_2
]})
self
.
assertEqual
(
parent_block
.
merged_group_access
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
]},
)
self
.
assertEqual
(
child_block
.
merged_group_access
,
{
self
.
PARTITION_1
:
[
self
.
PARTITION_1_GROUP_1
],
self
.
PARTITION_2
:
[
self
.
PARTITION_1_GROUP_2
]},
)
openedx/core/djangoapps/course_groups/partition_scheme.py
View file @
6fb3daa7
...
@@ -3,6 +3,8 @@ Provides a UserPartition driver for cohorts.
...
@@ -3,6 +3,8 @@ Provides a UserPartition driver for cohorts.
"""
"""
import
logging
import
logging
from
xmodule.partitions.partitions
import
NoSuchUserPartitionGroupError
from
.cohorts
import
get_cohort
,
get_partition_group_id_for_cohort
from
.cohorts
import
get_cohort
,
get_partition_group_id_for_cohort
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -56,8 +58,9 @@ class CohortPartitionScheme(object):
...
@@ -56,8 +58,9 @@ class CohortPartitionScheme(object):
# fail silently
# fail silently
return
None
return
None
group
=
user_partition
.
get_group
(
group_id
)
try
:
if
group
is
None
:
return
user_partition
.
get_group
(
group_id
)
except
NoSuchUserPartitionGroupError
:
# if we have a match but the group doesn't exist in the partition,
# if we have a match but the group doesn't exist in the partition,
# it means the mapping is invalid. the previous state of the
# it means the mapping is invalid. the previous state of the
# partition configuration may have been modified.
# partition configuration may have been modified.
...
@@ -67,9 +70,8 @@ class CohortPartitionScheme(object):
...
@@ -67,9 +70,8 @@ class CohortPartitionScheme(object):
"requested_partition_id"
:
user_partition
.
id
,
"requested_partition_id"
:
user_partition
.
id
,
"requested_group_id"
:
group_id
,
"requested_group_id"
:
group_id
,
"cohort_id"
:
cohort
.
id
,
"cohort_id"
:
cohort
.
id
,
}
},
exc_info
=
True
)
)
# fail silently
# fail silently
return
None
return
None
return
group
openedx/core/djangoapps/user_api/partition_schemes.py
View file @
6fb3daa7
"""
"""
Provides partition support to the user service.
Provides partition support to the user service.
"""
"""
import
logging
import
random
import
random
import
api.course_tag
as
course_tag_api
import
api.course_tag
as
course_tag_api
from
xmodule.partitions.partitions
import
UserPartitionError
from
xmodule.partitions.partitions
import
UserPartitionError
,
NoSuchUserPartitionGroupError
log
=
logging
.
getLogger
(
__name__
)
class
RandomUserPartitionScheme
(
object
):
class
RandomUserPartitionScheme
(
object
):
...
@@ -22,7 +24,22 @@ class RandomUserPartitionScheme(object):
...
@@ -22,7 +24,22 @@ class RandomUserPartitionScheme(object):
"""
"""
partition_key
=
cls
.
_key_for_partition
(
user_partition
)
partition_key
=
cls
.
_key_for_partition
(
user_partition
)
group_id
=
course_tag_api
.
get_course_tag
(
user
,
course_id
,
partition_key
)
group_id
=
course_tag_api
.
get_course_tag
(
user
,
course_id
,
partition_key
)
group
=
user_partition
.
get_group
(
int
(
group_id
))
if
not
group_id
is
None
else
None
group
=
None
if
group_id
is
not
None
:
# attempt to look up the presently assigned group
try
:
group
=
user_partition
.
get_group
(
int
(
group_id
))
except
NoSuchUserPartitionGroupError
:
# jsa: we can turn off warnings here if this is an expected case.
log
.
warn
(
"group not found in RandomUserPartitionScheme:
%
r"
,
{
"requested_partition_id"
:
user_partition
.
id
,
"requested_group_id"
:
group_id
,
},
exc_info
=
True
)
if
group
is
None
and
assign
:
if
group
is
None
and
assign
:
if
not
user_partition
.
groups
:
if
not
user_partition
.
groups
:
...
...
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