# 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 xmodule.modulestore.django import modulestore NAMESPACE_CHOICES = { 'ENTRANCE_EXAM': 'entrance_exams' } def get_namespace_choices(): """ Return the enum to the caller """ return NAMESPACE_CHOICES 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 fulfillment milestone for course referred by `prerequisite_course_key`. """ if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False): return None from milestones import api as milestones_api milestone_name = _('Course {course_id} requires {prerequisite_course_id}').format( course_id=unicode(course_key), prerequisite_course_id=unicode(prerequisite_course_key) ) milestone = milestones_api.add_milestone({ 'name': milestone_name, 'namespace': unicode(prerequisite_course_key), 'description': _('System defined milestone'), }) # add requirement course milestone milestones_api.add_course_milestone(course_key, 'requires', milestone) # add fulfillment course milestone milestones_api.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 not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False): return None from milestones import api as milestones_api milestones_api.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 not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False): return None from milestones import api as milestones_api #remove any existing requirement milestones with this pre-requisite course as requirement course_milestones = milestones_api.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', False): from milestones import api as milestones_api for course_key in enrolled_courses: required_courses = [] fulfilment_paths = milestones_api.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 not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api course_milestones = milestones_api.get_course_milestones(course_key=course_key, relationship="fulfills") for milestone in course_milestones: milestones_api.add_user_milestone({'id': user.id}, milestone) def get_required_content(course, user): """ Queries milestones subsystem to see if the specified course is gated on one or more milestones, and if those milestones can be fulfilled via completion of a particular course content module """ required_content = [] if settings.FEATURES.get('MILESTONES_APP', False): from milestones.exceptions import InvalidMilestoneRelationshipTypeException # Get all of the outstanding milestones for this course, for this user try: milestone_paths = get_course_milestones_fulfillment_paths( unicode(course.id), serialize_user(user) ) except InvalidMilestoneRelationshipTypeException: return required_content # For each outstanding milestone, see if this content is one of its fulfillment paths for path_key in milestone_paths: milestone_path = milestone_paths[path_key] if milestone_path.get('content') and len(milestone_path['content']): for content in milestone_path['content']: required_content.append(content) return required_content def milestones_achieved_by_user(user, namespace): """ It would fetch list of milestones completed by user """ if not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api return milestones_api.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 not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones.models import MilestoneRelationshipType 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, } def add_milestone(milestone_data): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api return milestones_api.add_milestone(milestone_data) def get_milestones(namespace): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return [] from milestones import api as milestones_api return milestones_api.get_milestones(namespace) def get_milestone_relationship_types(): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return {} from milestones import api as milestones_api return milestones_api.get_milestone_relationship_types() def add_course_milestone(course_id, relationship, milestone): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api return milestones_api.add_course_milestone(course_id, relationship, milestone) def get_course_milestones(course_id): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return [] from milestones import api as milestones_api return milestones_api.get_course_milestones(course_id) def add_course_content_milestone(course_id, content_id, relationship, milestone): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api return milestones_api.add_course_content_milestone(course_id, content_id, relationship, milestone) def get_course_content_milestones(course_id, content_id, relationship): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return [] from milestones import api as milestones_api return milestones_api.get_course_content_milestones(course_id, content_id, relationship) def remove_content_references(content_id): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api return milestones_api.remove_content_references(content_id) def any_unfulfilled_milestones(course_id, user_id): """ Returns a boolean if user has any unfulfilled milestones """ if not settings.FEATURES.get('MILESTONES_APP', False): return False return bool( get_course_milestones_fulfillment_paths(course_id, {"id": user_id}) ) def get_course_milestones_fulfillment_paths(course_id, user_id): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api return milestones_api.get_course_milestones_fulfillment_paths( course_id, user_id ) def add_user_milestone(user, milestone): """ Client API operation adapter/wrapper """ if not settings.FEATURES.get('MILESTONES_APP', False): return None from milestones import api as milestones_api return milestones_api.add_user_milestone(user, milestone)