milestones_helpers.py 14 KB
Newer Older
1 2
# pylint: disable=invalid-name
"""
3
Utility library for working with the edx-milestones app
4 5 6 7
"""

from django.conf import settings
from django.utils.translation import ugettext as _
8 9

from courseware.models import StudentModule
10 11
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
12 13
from xmodule.modulestore.django import modulestore

14 15 16 17
NAMESPACE_CHOICES = {
    'ENTRANCE_EXAM': 'entrance_exams'
}

18

19 20 21 22 23 24 25
def get_namespace_choices():
    """
    Return the enum to the caller
    """
    return NAMESPACE_CHOICES


26 27 28 29 30 31 32
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`.
    """
33 34 35 36 37 38 39 40 41 42 43 44 45 46
    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)
47

48 49
    # add fulfillment course milestone
    milestones_api.add_course_milestone(prerequisite_course_key, 'fulfills', milestone)
50 51 52 53 54 55 56


def remove_prerequisite_course(course_key, milestone):
    """
    It would remove pre-requisite course milestone for course
    referred by `course_key`.
    """
57 58 59 60 61 62 63
    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,
    )
64 65 66 67 68 69 70 71 72


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` .
    """
73 74 75 76 77 78 79 80
    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)
81

82 83 84 85 86
    # 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)
87 88 89 90 91 92 93 94 95 96


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 = {}
97 98
    if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
        from milestones import api as milestones_api
99 100
        for course_key in enrolled_courses:
            required_courses = []
101
            fulfilment_paths = milestones_api.get_course_milestones_fulfillment_paths(course_key, {'id': user.id})
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
            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
122 123
    and return list of dictionary with course key as 'key' field
    and course display name as `display` field.
124 125 126 127 128 129
    """
    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)
130 131 132 133 134
            prc = {
                'key': course_key,
                'display': get_course_display_name(required_course_descriptor)
            }
            pre_requisite_courses.append(prc)
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    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.
    """
153 154 155 156 157 158
    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)
159 160


161 162 163 164 165 166 167
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):
168 169 170
        from milestones import api as milestones_api
        from milestones.exceptions import InvalidMilestoneRelationshipTypeException

171 172
        # Get all of the outstanding milestones for this course, for this user
        try:
173
            milestone_paths = milestones_api.get_course_milestones_fulfillment_paths(
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
                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)

    #local imports to avoid circular reference
    from student.models import EntranceExamConfiguration
    can_skip_entrance_exam = EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id)
    # check if required_content has any entrance exam and user is allowed to skip it
    # then remove it from required content
    if required_content and getattr(course, 'entrance_exam_enabled', False) and can_skip_entrance_exam:
        descriptors = [modulestore().get_item(UsageKey.from_string(content)) for content in required_content]
        entrance_exam_contents = [unicode(descriptor.location)
                                  for descriptor in descriptors if descriptor.is_entrance_exam]
        required_content = list(set(required_content) - set(entrance_exam_contents))
    return required_content


200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
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


229 230 231 232
def milestones_achieved_by_user(user, namespace):
    """
    It would fetch list of milestones completed by user
    """
233 234 235 236
    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)
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253


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
    """
254 255 256 257 258
    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')
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276


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,
    }
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379


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 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)