Files
edx-platform/common/djangoapps/util/milestones_helpers.py

226 lines
8.5 KiB
Python

# pylint: disable=invalid-name
"""
Utility library for working with the edx-milestones app
"""
from django.conf import settings
from django.utils.translation import ugettext as _
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from courseware.models import StudentModule
from xmodule.modulestore.django import modulestore
from milestones.api import (
get_course_milestones,
add_milestone,
add_course_milestone,
remove_course_milestone,
get_course_milestones_fulfillment_paths,
add_user_milestone,
get_user_milestones,
)
from milestones.models import MilestoneRelationshipType
NAMESPACE_CHOICES = {
'ENTRANCE_EXAM': 'entrance_exams'
}
def add_prerequisite_course(course_key, prerequisite_course_key):
"""
It would create a milestone, then it would set newly created
milestones as requirement for course referred by `course_key`
and it would set newly created milestone as fulfilment
milestone for course referred by `prerequisite_course_key`.
"""
if settings.FEATURES.get('MILESTONES_APP', False):
# create a milestone
milestone = add_milestone({
'name': _('Course {} requires {}'.format(unicode(course_key), unicode(prerequisite_course_key))),
'namespace': unicode(prerequisite_course_key),
'description': _('System defined milestone'),
})
# add requirement course milestone
add_course_milestone(course_key, 'requires', milestone)
# add fulfillment course milestone
add_course_milestone(prerequisite_course_key, 'fulfills', milestone)
def remove_prerequisite_course(course_key, milestone):
"""
It would remove pre-requisite course milestone for course
referred by `course_key`.
"""
if settings.FEATURES.get('MILESTONES_APP', False):
remove_course_milestone(
course_key,
milestone,
)
def set_prerequisite_courses(course_key, prerequisite_course_keys):
"""
It would remove any existing requirement milestones for the given `course_key`
and create new milestones for each pre-requisite course in `prerequisite_course_keys`.
To only remove course milestones pass `course_key` and empty list or
None as `prerequisite_course_keys` .
"""
if settings.FEATURES.get('MILESTONES_APP', False):
#remove any existing requirement milestones with this pre-requisite course as requirement
course_milestones = get_course_milestones(course_key=course_key, relationship="requires")
if course_milestones:
for milestone in course_milestones:
remove_prerequisite_course(course_key, milestone)
# add milestones if pre-requisite course is selected
if prerequisite_course_keys:
for prerequisite_course_key_string in prerequisite_course_keys:
prerequisite_course_key = CourseKey.from_string(prerequisite_course_key_string)
add_prerequisite_course(course_key, prerequisite_course_key)
def get_pre_requisite_courses_not_completed(user, enrolled_courses):
"""
It would make dict of prerequisite courses not completed by user among courses
user has enrolled in. It calls the fulfilment api of milestones app and
iterates over all fulfilment milestones not achieved to make dict of
prerequisite courses yet to be completed.
"""
pre_requisite_courses = {}
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES'):
for course_key in enrolled_courses:
required_courses = []
fulfilment_paths = get_course_milestones_fulfillment_paths(course_key, {'id': user.id})
for milestone_key, milestone_value in fulfilment_paths.items(): # pylint: disable=unused-variable
for key, value in milestone_value.items():
if key == 'courses' and value:
for required_course in value:
required_course_key = CourseKey.from_string(required_course)
required_course_descriptor = modulestore().get_course(required_course_key)
required_courses.append({
'key': required_course_key,
'display': get_course_display_name(required_course_descriptor)
})
# if there are required courses add to dict
if required_courses:
pre_requisite_courses[course_key] = {'courses': required_courses}
return pre_requisite_courses
def get_prerequisite_courses_display(course_descriptor):
"""
It would retrieve pre-requisite courses, make display strings
and return list of dictionary with course key as 'key' field
and course display name as `display` field.
"""
pre_requisite_courses = []
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False) and course_descriptor.pre_requisite_courses:
for course_id in course_descriptor.pre_requisite_courses:
course_key = CourseKey.from_string(course_id)
required_course_descriptor = modulestore().get_course(course_key)
prc = {
'key': course_key,
'display': get_course_display_name(required_course_descriptor)
}
pre_requisite_courses.append(prc)
return pre_requisite_courses
def get_course_display_name(descriptor):
"""
It would return display name from given course descriptor
"""
return ' '.join([
descriptor.display_org_with_default,
descriptor.display_number_with_default
])
def fulfill_course_milestone(course_key, user):
"""
Marks the course specified by the given course_key as complete for the given user.
If any other courses require this course as a prerequisite, their milestones will be appropriately updated.
"""
if settings.FEATURES.get('MILESTONES_APP', False):
course_milestones = get_course_milestones(course_key=course_key, relationship="fulfills")
for milestone in course_milestones:
add_user_milestone({'id': user.id}, milestone)
def calculate_entrance_exam_score(user, course_descriptor, exam_modules):
"""
Calculates the score (percent) of the entrance exam using the provided modules
"""
exam_module_ids = [exam_module.location for exam_module in exam_modules]
student_modules = StudentModule.objects.filter(
student=user,
course_id=course_descriptor.id,
module_state_key__in=exam_module_ids,
)
exam_pct = 0
if student_modules:
module_pcts = []
ignore_categories = ['course', 'chapter', 'sequential', 'vertical']
for module in exam_modules:
if module.graded and module.category not in ignore_categories:
module_pct = 0
try:
student_module = student_modules.get(module_state_key=module.location)
if student_module.max_grade:
module_pct = student_module.grade / student_module.max_grade
module_pcts.append(module_pct)
except StudentModule.DoesNotExist:
pass
if module_pcts:
exam_pct = sum(module_pcts) / float(len(module_pcts))
return exam_pct
def milestones_achieved_by_user(user, namespace):
"""
It would fetch list of milestones completed by user
"""
if settings.FEATURES.get('MILESTONES_APP', False):
return get_user_milestones({'id': user.id}, namespace)
def is_valid_course_key(key):
"""
validates course key. returns True if valid else False.
"""
try:
course_key = CourseKey.from_string(key)
except InvalidKeyError:
course_key = key
return isinstance(course_key, CourseKey)
def seed_milestone_relationship_types():
"""
Helper method to pre-populate MRTs so the tests can run
"""
if settings.FEATURES.get('MILESTONES_APP', False):
MilestoneRelationshipType.objects.create(name='requires')
MilestoneRelationshipType.objects.create(name='fulfills')
def generate_milestone_namespace(namespace, course_key=None):
"""
Returns a specifically-formatted namespace string for the specified type
"""
if namespace in NAMESPACE_CHOICES.values():
if namespace == 'entrance_exams':
return '{}.{}'.format(unicode(course_key), NAMESPACE_CHOICES['ENTRANCE_EXAM'])
def serialize_user(user):
"""
Returns a milestones-friendly representation of a user object
"""
return {
'id': user.id,
}