Skip to content
Snippets Groups Projects
Commit 99f8918b authored by Calen Pennington's avatar Calen Pennington
Browse files

Add an automatic UserPartition and Scheme for Content Type Gating.

For now, this UserPartition groups all users as Full-access users.
parent 0adb6547
No related merge requests found
......@@ -8,7 +8,15 @@ from django.utils.translation import ugettext_lazy as _
import logging
from openedx.core.lib.cache_utils import request_cached
from xmodule.partitions.partitions import UserPartition, UserPartitionError, ENROLLMENT_TRACK_PARTITION_ID
from openedx.features.content_type_gating.partitions import (
CONTENT_GATING_PARTITION_ID,
create_content_gating_partition,
)
from xmodule.partitions.partitions import (
UserPartition,
UserPartitionError,
ENROLLMENT_TRACK_PARTITION_ID,
)
from xmodule.modulestore.django import modulestore
......@@ -42,8 +50,14 @@ def _get_dynamic_partitions(course):
Return the dynamic user partitions for this course.
If none exists, returns an empty array.
"""
enrollment_partition = _create_enrollment_track_partition(course)
return [enrollment_partition] if enrollment_partition else []
return [
partition
for partition in [
_create_enrollment_track_partition(course),
create_content_gating_partition(course),
]
if partition
]
def _create_enrollment_track_partition(course):
......
......@@ -127,6 +127,19 @@ class SplitTestModuleLMSTest(SplitTestModuleTest):
"""
shard = 1
def setUp(self):
super(SplitTestModuleLMSTest, self).setUp()
content_gating_flag_patcher = patch(
'openedx.features.content_type_gating.partitions.CONTENT_TYPE_GATING_FLAG.is_enabled',
return_value=False,
).start()
self.addCleanup(content_gating_flag_patcher.stop)
content_gating_ui_flag_patcher = patch(
'openedx.features.content_type_gating.partitions.CONTENT_TYPE_GATING_STUDIO_UI_FLAG.is_enabled',
return_value=False,
).start()
self.addCleanup(content_gating_ui_flag_patcher.stop)
@ddt.data((0, 'split_test_cond0'), (1, 'split_test_cond1'))
@ddt.unpack
def test_child(self, user_tag, child_url_name):
......
......@@ -7,7 +7,13 @@ from django.conf import settings
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from .transformers import library_content, start_date, user_partitions, visibility, load_override_data
from .transformers import (
library_content,
start_date,
user_partitions,
visibility,
load_override_data,
)
from .usage_info import CourseUsageInfo
INDIVIDUAL_STUDENT_OVERRIDE_PROVIDER = (
......
"""
Define the content_type_gating Django App.
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
......
"""
Define the ContentTypeGatingPartition and ContentTypeGatingPartitionScheme.
These are used together to allow course content to be blocked for a subset
of audit learners.
"""
import logging
from django.utils.translation import ugettext_lazy as _
from lms.djangoapps.courseware.masquerade import (
get_course_masquerade,
is_masquerading_as_specific_student,
get_masquerading_user_group,
)
from xmodule.partitions.partitions import Group, UserPartition, UserPartitionError
LOG = logging.getLogger(__name__)
# Studio generates partition IDs starting at 100. There is already a manually generated
# partition for Enrollment Track that uses ID 50, so we'll use 51.
CONTENT_GATING_PARTITION_ID = 51
CONTENT_TYPE_GATE_GROUP_IDS = {
'limited_access': 1,
'full_access': 2,
}
def create_content_gating_partition(course):
"""
Create and return the Content Gating user partition.
"""
try:
content_gate_scheme = UserPartition.get_scheme("content_type_gate")
except UserPartitionError:
LOG.warning("No 'content_type_gate' scheme registered, ContentTypeGatingPartitionScheme will not be created.")
return None
used_ids = set(p.id for p in course.user_partitions)
if CONTENT_GATING_PARTITION_ID in used_ids:
# It's possible for course authors to add arbitrary partitions via XML import. If they do, and create a
# partition with id 51, it will collide with the Content Gating Partition. We'll catch that here, and
# then fix the course content as needed (or get the course team to).
LOG.warning(
"Can't add 'content_type_gate' partition, as ID {id} is assigned to {partition} in course {course}.".format(
id=CONTENT_GATING_PARTITION_ID,
partition=_get_partition_from_id(course.user_partitions, CONTENT_GATING_PARTITION_ID).name,
course=unicode(course.id)
)
)
return None
partition = content_gate_scheme.create_user_partition(
id=CONTENT_GATING_PARTITION_ID,
name=_(u"Content Type Gating"),
description=_(u"Partition for segmenting users by access to gated content types"),
parameters={"course_id": unicode(course.id)}
)
return partition
class ContentTypeGatingPartition(UserPartition):
pass
class ContentTypeGatingPartitionScheme(object):
"""
This scheme implements the Content Type Gating permission partitioning.
This partitioning is roughly the same as the verified/audit split, but also allows for individual
schools or courses to specify particular learner subsets by email that are allowed to access
the gated content despite not being verified users.
"""
LIMITED_ACCESS = Group(CONTENT_TYPE_GATE_GROUP_IDS['limited_access'], 'Limited-access Users')
FULL_ACCESS = Group(CONTENT_TYPE_GATE_GROUP_IDS['full_access'], 'Full-access Users')
@classmethod
def get_group_for_user(cls, course_key, user, user_partition, **kwargs): # pylint: disable=unused-argument
"""
Returns the Group for the specified user.
"""
# 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):
return get_masquerading_user_group(course_key, user, user_partition)
# For now, treat everyone as a Full-access user, until we have the rest of the
# feature gating logic in place.
return cls.FULL_ACCESS
@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 ContentTypeGatingPartition(
id,
unicode(name),
unicode(description),
[
cls.LIMITED_ACCESS,
cls.FULL_ACCESS,
],
cls,
parameters,
# N.B. This forces Content Type Gating partitioning to always be active on every course,
# no matter how the course xml content is set. We will manage enabling/disabling
# as a policy in the LMS.
active=True,
)
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
......@@ -47,6 +47,7 @@ setup(
"cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme",
"verification = openedx.core.djangoapps.user_api.partition_schemes:ReturnGroup1PartitionScheme",
"enrollment_track = openedx.core.djangoapps.verified_track_content.partition_scheme:EnrollmentTrackPartitionScheme",
"content_type_gate = openedx.features.content_type_gating.partitions:ContentTypeGatingPartitionScheme",
],
"openedx.block_structure_transformer": [
"library_content = lms.djangoapps.course_blocks.transformers.library_content:ContentLibraryTransformer",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment