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:
Calen Pennington
2018-10-15 13:20:38 -04:00
committed by Bessie Steinberg
parent 2ff3a38d91
commit 83d676cbfa
13 changed files with 242 additions and 38 deletions

View 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)

View 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

View File

@@ -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