Files
edx-platform/openedx/core/lib/gating/api.py
2016-01-28 14:05:20 -05:00

298 lines
9.6 KiB
Python

"""
API for the gating djangoapp
"""
import logging
from django.utils.translation import ugettext as _
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
"""
# 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}
)
]