proctoring.py 5.45 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
"""
Code related to the handling of Proctored Exams in Studio
"""

import logging

from django.conf import settings

from xmodule.modulestore.django import modulestore

from contentstore.views.helpers import is_item_in_course_tree

from edx_proctoring.api import (
    get_exam_by_content_id,
    update_exam,
    create_exam,
    get_all_exams_for_course,
Muhammad Shoaib committed
18 19 20
    update_review_policy,
    create_exam_review_policy,
    remove_review_policy,
21 22
)
from edx_proctoring.exceptions import (
Muhammad Shoaib committed
23 24
    ProctoredExamNotFoundException,
    ProctoredExamReviewPolicyNotFoundException
25 26 27 28 29
)

log = logging.getLogger(__name__)


30
def register_special_exams(course_key):
31 32 33 34
    """
    This is typically called on a course published signal. The course is examined for sequences
    that are marked as timed exams. Then these are registered with the edx-proctoring
    subsystem. Likewise, if formerly registered exams are unmarked, then those
35
    registered exams are marked as inactive
36 37
    """

38
    if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
39 40 41 42
        # if feature is not enabled then do a quick exit
        return

    course = modulestore().get_course(course_key)
43 44 45
    if not course.enable_proctored_exams and not course.enable_timed_exams:
        # likewise if course does not have these features turned on
        # then quickly exit
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
        return

    # get all sequences, since they can be marked as timed/proctored exams
    _timed_exams = modulestore().get_items(
        course_key,
        qualifiers={
            'category': 'sequential',
        },
        settings={
            'is_time_limited': True,
        }
    )

    # filter out any potential dangling sequences
    timed_exams = [
        timed_exam
        for timed_exam in _timed_exams
        if is_item_in_course_tree(timed_exam)
    ]

    # enumerate over list of sequences which are time-limited and
    # add/update any exam entries in edx-proctoring
    for timed_exam in timed_exams:
        msg = (
            'Found {location} as a timed-exam in course structure. Inspecting...'.format(
                location=unicode(timed_exam.location)
            )
        )
        log.info(msg)

        try:
            exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location))
            # update case, make sure everything is synced
Muhammad Shoaib committed
79
            exam_id = update_exam(
80 81 82
                exam_id=exam['id'],
                exam_name=timed_exam.display_name,
                time_limit_mins=timed_exam.default_time_limit_minutes,
83 84
                due_date=timed_exam.due,
                is_proctored=timed_exam.is_proctored_exam,
85
                is_practice_exam=timed_exam.is_practice_exam,
86 87
                is_active=True,
                hide_after_due=timed_exam.hide_after_due,
88 89 90
            )
            msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
            log.info(msg)
Muhammad Shoaib committed
91

92 93 94 95 96 97
        except ProctoredExamNotFoundException:
            exam_id = create_exam(
                course_id=unicode(course_key),
                content_id=unicode(timed_exam.location),
                exam_name=timed_exam.display_name,
                time_limit_mins=timed_exam.default_time_limit_minutes,
98 99
                due_date=timed_exam.due,
                is_proctored=timed_exam.is_proctored_exam,
100
                is_practice_exam=timed_exam.is_practice_exam,
101 102
                is_active=True,
                hide_after_due=timed_exam.hide_after_due,
103 104 105 106
            )
            msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id)
            log.info(msg)

Muhammad Shoaib committed
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
        # only create/update exam policy for the proctored exams
        if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam:
            try:
                update_review_policy(
                    exam_id=exam_id,
                    set_by_user_id=timed_exam.edited_by,
                    review_policy=timed_exam.exam_review_rules
                )
            except ProctoredExamReviewPolicyNotFoundException:
                if timed_exam.exam_review_rules:  # won't save an empty rule.
                    create_exam_review_policy(
                        exam_id=exam_id,
                        set_by_user_id=timed_exam.edited_by,
                        review_policy=timed_exam.exam_review_rules
                    )
                    msg = 'Created new exam review policy with exam_id {exam_id}'.format(exam_id=exam_id)
                    log.info(msg)
        else:
            try:
                # remove any associated review policy
                remove_review_policy(exam_id=exam_id)
            except ProctoredExamReviewPolicyNotFoundException:
                pass

131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    # then see which exams we have in edx-proctoring that are not in
    # our current list. That means the the user has disabled it
    exams = get_all_exams_for_course(course_key)

    for exam in exams:
        if exam['is_active']:
            # try to look up the content_id in the sequences location

            search = [
                timed_exam for timed_exam in timed_exams if
                unicode(timed_exam.location) == exam['content_id']
            ]
            if not search:
                # This means it was turned off in Studio, we need to mark
                # the exam as inactive (we don't delete!)
                msg = 'Disabling timed exam {exam_id}'.format(exam_id=exam['id'])
                log.info(msg)
                update_exam(
                    exam_id=exam['id'],
                    is_proctored=False,
                    is_active=False,
                )