301 lines
9.8 KiB
Python
301 lines
9.8 KiB
Python
"""
|
|
API for the gating djangoapp
|
|
"""
|
|
import logging
|
|
|
|
from django.utils.translation import ugettext as _
|
|
from lms.djangoapps.courseware.access import _has_access_to_course
|
|
from milestones import api as milestones_api
|
|
from opaque_keys.edx.keys import UsageKey
|
|
from xmodule.modulestore.django import modulestore
|
|
from openedx.core.lib.gating.exceptions import GatingValidationError
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# This is used to namespace gating-specific milestones
|
|
GATING_NAMESPACE_QUALIFIER = '.gating'
|
|
|
|
|
|
def _get_prerequisite_milestone(prereq_content_key):
|
|
"""
|
|
Get gating milestone associated with the given content usage key.
|
|
|
|
Arguments:
|
|
prereq_content_key (str|UsageKey): The content usage key
|
|
|
|
Returns:
|
|
dict: Milestone dict
|
|
"""
|
|
milestones = milestones_api.get_milestones("{usage_key}{qualifier}".format(
|
|
usage_key=prereq_content_key,
|
|
qualifier=GATING_NAMESPACE_QUALIFIER
|
|
))
|
|
|
|
if not milestones:
|
|
log.warning("Could not find gating milestone for prereq UsageKey %s", prereq_content_key)
|
|
return None
|
|
|
|
if len(milestones) > 1:
|
|
# We should only ever have one gating milestone per UsageKey
|
|
# Log a warning here and pick the first one
|
|
log.warning("Multiple gating milestones found for prereq UsageKey %s", prereq_content_key)
|
|
|
|
return milestones[0]
|
|
|
|
|
|
def _validate_min_score(min_score):
|
|
"""
|
|
Validates the minimum score entered by the Studio user.
|
|
|
|
Arguments:
|
|
min_score (str|int): The minimum score to validate
|
|
|
|
Returns:
|
|
None
|
|
|
|
Raises:
|
|
GatingValidationError: If the minimum score is not valid
|
|
"""
|
|
if min_score:
|
|
message = _("%(min_score)s is not a valid grade percentage") % {'min_score': min_score}
|
|
try:
|
|
min_score = int(min_score)
|
|
except ValueError:
|
|
raise GatingValidationError(message)
|
|
|
|
if min_score < 0 or min_score > 100:
|
|
raise GatingValidationError(message)
|
|
|
|
|
|
def gating_enabled(default=None):
|
|
"""
|
|
Decorator that checks the enable_subsection_gating course flag to
|
|
see if the subsection gating feature is active for a given course.
|
|
If not, calls to the decorated function return the specified default value.
|
|
|
|
Arguments:
|
|
default (ANY): The value to return if the enable_subsection_gating course flag is False
|
|
|
|
Returns:
|
|
ANY: The specified default value if the gating feature is off,
|
|
otherwise the result of the decorated function
|
|
"""
|
|
def wrap(f): # pylint: disable=missing-docstring
|
|
def function_wrapper(course, *args): # pylint: disable=missing-docstring
|
|
if not course.enable_subsection_gating:
|
|
return default
|
|
return f(course, *args)
|
|
return function_wrapper
|
|
return wrap
|
|
|
|
|
|
def find_gating_milestones(course_key, content_key=None, relationship=None, user=None):
|
|
"""
|
|
Finds gating milestone dicts related to the given supplied parameters.
|
|
|
|
Arguments:
|
|
course_key (str|CourseKey): The course key
|
|
content_key (str|UsageKey): The content usage key
|
|
relationship (str): The relationship type (e.g. 'requires')
|
|
user (dict): The user dict (e.g. {'id': 4})
|
|
|
|
Returns:
|
|
list: A list of milestone dicts
|
|
"""
|
|
return [
|
|
m for m in milestones_api.get_course_content_milestones(course_key, content_key, relationship, user)
|
|
if GATING_NAMESPACE_QUALIFIER in m.get('namespace')
|
|
]
|
|
|
|
|
|
def get_gating_milestone(course_key, content_key, relationship):
|
|
"""
|
|
Gets a single gating milestone dict related to the given supplied parameters.
|
|
|
|
Arguments:
|
|
course_key (str|CourseKey): The course key
|
|
content_key (str|UsageKey): The content usage key
|
|
relationship (str): The relationship type (e.g. 'requires')
|
|
|
|
Returns:
|
|
dict or None: The gating milestone dict or None
|
|
"""
|
|
try:
|
|
return find_gating_milestones(course_key, content_key, relationship)[0]
|
|
except IndexError:
|
|
return None
|
|
|
|
|
|
def get_prerequisites(course_key):
|
|
"""
|
|
Find all the gating milestones associated with a course and the
|
|
XBlock info associated with those gating milestones.
|
|
|
|
Arguments:
|
|
course_key (str|CourseKey): The course key
|
|
|
|
Returns:
|
|
list: A list of dicts containing the milestone and associated XBlock info
|
|
"""
|
|
course_content_milestones = find_gating_milestones(course_key)
|
|
|
|
milestones_by_block_id = {}
|
|
block_ids = []
|
|
for milestone in course_content_milestones:
|
|
prereq_content_key = milestone['namespace'].replace(GATING_NAMESPACE_QUALIFIER, '')
|
|
block_id = UsageKey.from_string(prereq_content_key).block_id
|
|
block_ids.append(block_id)
|
|
milestones_by_block_id[block_id] = milestone
|
|
|
|
result = []
|
|
for block in modulestore().get_items(course_key, qualifiers={'name': block_ids}):
|
|
milestone = milestones_by_block_id.get(block.location.block_id)
|
|
if milestone:
|
|
milestone['block_display_name'] = block.display_name
|
|
milestone['block_usage_key'] = unicode(block.location)
|
|
result.append(milestone)
|
|
|
|
return result
|
|
|
|
|
|
def add_prerequisite(course_key, prereq_content_key):
|
|
"""
|
|
Creates a new Milestone and CourseContentMilestone indicating that
|
|
the given course content fulfills a prerequisite for gating
|
|
|
|
Arguments:
|
|
course_key (str|CourseKey): The course key
|
|
prereq_content_key (str|UsageKey): The prerequisite content usage key
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
milestone = milestones_api.add_milestone(
|
|
{
|
|
'name': _('Gating milestone for {usage_key}').format(usage_key=unicode(prereq_content_key)),
|
|
'namespace': "{usage_key}{qualifier}".format(
|
|
usage_key=prereq_content_key,
|
|
qualifier=GATING_NAMESPACE_QUALIFIER
|
|
),
|
|
'description': _('System defined milestone'),
|
|
},
|
|
propagate=False
|
|
)
|
|
milestones_api.add_course_content_milestone(course_key, prereq_content_key, 'fulfills', milestone)
|
|
|
|
|
|
def remove_prerequisite(prereq_content_key):
|
|
"""
|
|
Removes the Milestone and CourseContentMilestones related to the gating
|
|
prerequisite which the given course content fulfills
|
|
|
|
Arguments:
|
|
prereq_content_key (str|UsageKey): The prerequisite content usage key
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
milestones = milestones_api.get_milestones("{usage_key}{qualifier}".format(
|
|
usage_key=prereq_content_key,
|
|
qualifier=GATING_NAMESPACE_QUALIFIER
|
|
))
|
|
for milestone in milestones:
|
|
milestones_api.remove_milestone(milestone.get('id'))
|
|
|
|
|
|
def is_prerequisite(course_key, prereq_content_key):
|
|
"""
|
|
Returns True if there is at least one CourseContentMilestone
|
|
which the given course content fulfills
|
|
|
|
Arguments:
|
|
course_key (str|CourseKey): The course key
|
|
prereq_content_key (str|UsageKey): The prerequisite content usage key
|
|
|
|
Returns:
|
|
bool: True if the course content fulfills a CourseContentMilestone, otherwise False
|
|
"""
|
|
return get_gating_milestone(
|
|
course_key,
|
|
prereq_content_key,
|
|
'fulfills'
|
|
) is not None
|
|
|
|
|
|
def set_required_content(course_key, gated_content_key, prereq_content_key, min_score):
|
|
"""
|
|
Adds a `requires` milestone relationship for the given gated_content_key if a prerequisite
|
|
prereq_content_key is provided. If prereq_content_key is None, removes the `requires`
|
|
milestone relationship.
|
|
|
|
Arguments:
|
|
course_key (str|CourseKey): The course key
|
|
gated_content_key (str|UsageKey): The gated content usage key
|
|
prereq_content_key (str|UsageKey): The prerequisite content usage key
|
|
min_score (str|int): The minimum score
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
milestone = None
|
|
for gating_milestone in find_gating_milestones(course_key, gated_content_key, 'requires'):
|
|
if not prereq_content_key or prereq_content_key not in gating_milestone.get('namespace'):
|
|
milestones_api.remove_course_content_milestone(course_key, gated_content_key, gating_milestone)
|
|
else:
|
|
milestone = gating_milestone
|
|
|
|
if prereq_content_key:
|
|
_validate_min_score(min_score)
|
|
requirements = {'min_score': min_score}
|
|
if not milestone:
|
|
milestone = _get_prerequisite_milestone(prereq_content_key)
|
|
milestones_api.add_course_content_milestone(course_key, gated_content_key, 'requires', milestone, requirements)
|
|
|
|
|
|
def get_required_content(course_key, gated_content_key):
|
|
"""
|
|
Returns the prerequisite content usage key and minimum score needed for fulfillment
|
|
of that prerequisite for the given gated_content_key.
|
|
|
|
Args:
|
|
course_key (str|CourseKey): The course key
|
|
gated_content_key (str|UsageKey): The gated content usage key
|
|
|
|
Returns:
|
|
tuple: The prerequisite content usage key and minimum score, (None, None) if the content is not gated
|
|
"""
|
|
milestone = get_gating_milestone(course_key, gated_content_key, 'requires')
|
|
if milestone:
|
|
return (
|
|
milestone.get('namespace', '').replace(GATING_NAMESPACE_QUALIFIER, ''),
|
|
milestone.get('requirements', {}).get('min_score')
|
|
)
|
|
else:
|
|
return None, None
|
|
|
|
|
|
@gating_enabled(default=[])
|
|
def get_gated_content(course, user):
|
|
"""
|
|
Returns the unfulfilled gated content usage keys in the given course.
|
|
|
|
Arguments:
|
|
course (CourseDescriptor): The course
|
|
user (User): The user
|
|
|
|
Returns:
|
|
list: The list of gated content usage keys for the given course
|
|
"""
|
|
if _has_access_to_course(user, 'staff', course.id):
|
|
return []
|
|
else:
|
|
# Get the unfulfilled gating milestones for this course, for this user
|
|
return [
|
|
m['content_id'] for m in find_gating_milestones(
|
|
course.id,
|
|
None,
|
|
'requires',
|
|
{'id': user.id}
|
|
)
|
|
]
|