Add Content Type Gating Behind Waffle Flag
Content Type Gating: Xblocks that have a graded component cannot be
accessed by audit track users.
- Caveats:
- In studio, instructors can set certain xblocks to be available to
all users, but graded components will default to not being
available for audit users
- If a course does not have a verified mode option, all users will
have access to graded content.
The Waffle Flag: The waffle flag is of for now.
It's name is: ```content_type_gating.debug```
This Commit Does NOT Include: Displaying for a user WHY they do not have
access to a specific piece of content. That change will be part of
another PR.
This commit is contained in:
committed by
Bessie Steinberg
parent
2ff3a38d91
commit
83d676cbfa
49
openedx/features/content_type_gating/block_transformers.py
Normal file
49
openedx/features/content_type_gating/block_transformers.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Content Type Gate Transformer implementation.
|
||||
Limits access for certain users to certain types of content.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from openedx.core.djangoapps.content.block_structure.transformer import (
|
||||
BlockStructureTransformer,
|
||||
)
|
||||
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
|
||||
|
||||
|
||||
class ContentTypeGateTransformer(BlockStructureTransformer):
|
||||
"""
|
||||
A transformer that adds a partition condition for all graded content
|
||||
so that the content is only visible to verified users.
|
||||
"""
|
||||
WRITE_VERSION = 1
|
||||
READ_VERSION = 1
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
"""
|
||||
Unique identifier for the transformer's class;
|
||||
same identifier used in setup.py.
|
||||
"""
|
||||
return "content_type_gate"
|
||||
|
||||
@classmethod
|
||||
def collect(cls, block_structure):
|
||||
"""
|
||||
Collects any information that's necessary to execute this
|
||||
transformer's transform method.
|
||||
"""
|
||||
block_structure.request_xblock_fields('group_access', 'graded', 'has_score')
|
||||
|
||||
def transform(self, usage_info, block_structure):
|
||||
for block_key in block_structure.topological_traversal():
|
||||
graded = block_structure.get_xblock_field(block_key, 'graded')
|
||||
has_score = block_structure.get_xblock_field(block_key, 'has_score')
|
||||
if graded and has_score:
|
||||
current_access = block_structure.get_xblock_field(block_key, 'group_access')
|
||||
if current_access is None:
|
||||
current_access = {}
|
||||
current_access.setdefault(
|
||||
CONTENT_GATING_PARTITION_ID,
|
||||
[settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']]
|
||||
)
|
||||
block_structure.override_xblock_field(block_key, 'group_access', current_access)
|
||||
40
openedx/features/content_type_gating/field_override.py
Normal file
40
openedx/features/content_type_gating/field_override.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
FieldOverride that forces graded components to be only accessible to
|
||||
students in the Unlocked Group of the ContentTypeGating partition.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider, disable_overrides
|
||||
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
|
||||
|
||||
|
||||
class ContentTypeGatingFieldOverride(FieldOverrideProvider):
|
||||
"""
|
||||
A concrete implementation of
|
||||
:class:`~courseware.field_overrides.FieldOverrideProvider` which forces
|
||||
graded content to only be accessible to the Full Access group
|
||||
"""
|
||||
def get(self, block, name, default):
|
||||
if name != 'group_access':
|
||||
return default
|
||||
|
||||
if not (getattr(block, 'graded', False) and block.has_score):
|
||||
return default
|
||||
|
||||
# Read the group_access from the fallback field-data service
|
||||
with disable_overrides():
|
||||
original_group_access = block.group_access
|
||||
|
||||
if original_group_access is None:
|
||||
original_group_access = {}
|
||||
original_group_access.setdefault(
|
||||
CONTENT_GATING_PARTITION_ID,
|
||||
[settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']]
|
||||
)
|
||||
|
||||
return original_group_access
|
||||
|
||||
@classmethod
|
||||
def enabled_for(cls, course):
|
||||
"""This simple override provider is always enabled"""
|
||||
return True
|
||||
@@ -7,8 +7,11 @@ of audit learners.
|
||||
|
||||
import logging
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.apps import apps
|
||||
from lms.djangoapps.courseware.masquerade import (
|
||||
get_course_masquerade,
|
||||
is_masquerading_as_specific_student,
|
||||
@@ -20,13 +23,13 @@ from openedx.features.course_duration_limits.config import (
|
||||
CONTENT_TYPE_GATING_STUDIO_UI_FLAG,
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
@@ -102,7 +105,48 @@ class ContentTypeGatingPartitionScheme(object):
|
||||
|
||||
# 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
|
||||
|
||||
if not CONTENT_TYPE_GATING_FLAG.is_enabled():
|
||||
return cls.FULL_ACCESS
|
||||
|
||||
# If CONTENT_TYPE_GATING is enabled use the following logic to determine whether a user should have FULL_ACCESS
|
||||
# or LIMITED_ACCESS
|
||||
|
||||
course_mode = apps.get_model('course_modes.CourseMode')
|
||||
modes = course_mode.modes_for_course(course_key, include_expired=True, only_selectable=False)
|
||||
modes_dict = {mode.slug: mode for mode in modes}
|
||||
|
||||
# If there is no verified mode, all users are granted FULL_ACCESS
|
||||
if not course_mode.has_verified_mode(modes_dict):
|
||||
return cls.FULL_ACCESS
|
||||
|
||||
course_enrollment = apps.get_model('student.CourseEnrollment')
|
||||
|
||||
mode_slug, is_active = course_enrollment.enrollment_mode_for_user(user, course_key)
|
||||
|
||||
if mode_slug and is_active:
|
||||
course_mode = course_mode.mode_for_course(
|
||||
course_key,
|
||||
mode_slug,
|
||||
modes=modes,
|
||||
)
|
||||
if course_mode is None:
|
||||
LOG.error(
|
||||
"User %s is in an unknown CourseMode '%s'"
|
||||
" for course %s. Granting full access to content for this user",
|
||||
user.username,
|
||||
mode_slug,
|
||||
course_key,
|
||||
)
|
||||
return cls.FULL_ACCESS
|
||||
|
||||
if mode_slug == CourseMode.AUDIT:
|
||||
return cls.LIMITED_ACCESS
|
||||
else:
|
||||
return cls.FULL_ACCESS
|
||||
else:
|
||||
# Unenrolled users don't get gated content
|
||||
return cls.LIMITED_ACCESS
|
||||
|
||||
@classmethod
|
||||
def create_user_partition(cls, id, name, description, groups=None, parameters=None, active=True): # pylint: disable=redefined-builtin, invalid-name, unused-argument
|
||||
|
||||
Reference in New Issue
Block a user