226 lines
8.5 KiB
Python
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,
|
|
}
|