Пример #1
0
    def test_boolean_fields(self):
        """
        Tests the boolean fields. Should cause a validation error in case a field is required.
        """
        data = {
            'id': "123",
            'course_id': "a/b/c",
            'exam_name': "midterm1",
            'content_id': '123aXqe0',
            'time_limit_mins': 90,
            'external_id': '123',
            'is_proctored': 'bla',
            'is_practice_exam': 'bla',
            'is_active': 'f',
            'hide_after_due': 't',
        }
        serializer = ProctoredExamSerializer(data=data)

        self.assertFalse(serializer.is_valid())
        self.assertDictEqual(
            {
                'is_proctored': [u'"bla" is not a valid boolean.'],
                'is_practice_exam': [u'"bla" is not a valid boolean.'],
            }, serializer.errors
        )
Пример #2
0
def get_all_exams_for_course(course_id):
    """
    This method will return all exams for a course. This will return a list
    of dictionaries, whose schema is the same as what is returned in
    get_exam_by_id
    Returns a list containing dictionary version of the Django ORM object
    e.g.
    [{
        "course_id": "edX/DemoX/Demo_Course",
        "content_id": "123",
        "external_id": "",
        "exam_name": "Midterm",
        "time_limit_mins": 90,
        "is_proctored": true,
        "is_active": true
    },
    {
        ...: ...,
        ...: ...

    },
    ..
    ]
    """
    exams = ProctoredExam.get_all_exams_for_course(course_id)

    return [
        ProctoredExamSerializer(proctored_exam).data
        for proctored_exam in exams
    ]
Пример #3
0
 def post(self, request):
     """
     Http POST handler. Creates an exam.
     """
     serializer = ProctoredExamSerializer(data=request.data)
     if serializer.is_valid():
         exam_id = create_exam(
             course_id=request.data.get('course_id', None),
             content_id=request.data.get('content_id', None),
             exam_name=request.data.get('exam_name', None),
             time_limit_mins=request.data.get('time_limit_mins', None),
             is_proctored=request.data.get('is_proctored', None),
             is_practice_exam=request.data.get('is_practice_exam', None),
             external_id=request.data.get('external_id', None),
             is_active=request.data.get('is_active', None))
         return Response({'exam_id': exam_id})
     else:
         return Response(status=status.HTTP_400_BAD_REQUEST,
                         data=serializer.errors)
Пример #4
0
 def post(self, request):
     """
     Http POST handler. Creates an exam.
     """
     serializer = ProctoredExamSerializer(data=request.data)
     if serializer.is_valid():
         exam_id = create_exam(
             course_id=request.data.get("course_id", None),
             content_id=request.data.get("content_id", None),
             exam_name=request.data.get("exam_name", None),
             time_limit_mins=request.data.get("time_limit_mins", None),
             is_proctored=request.data.get("is_proctored", None),
             is_practice_exam=request.data.get("is_practice_exam", None),
             external_id=request.data.get("external_id", None),
             is_active=request.data.get("is_active", None),
         )
         return Response({"exam_id": exam_id})
     else:
         return Response(status=status.HTTP_400_BAD_REQUEST, data=serializer.errors)
Пример #5
0
    def test_boolean_fields(self):
        """
        Tests the boolean fields. Should cause a validation error in case a field is required.
        """
        data = {
            'id': "123",
            'course_id': "a/b/c",
            'exam_name': "midterm1",
            'content_id': '123aXqe0',
            'time_limit_mins': 90,
            'external_id': '123',
            'is_proctored': 'bla',
            'is_practice_exam': 'bla',
            'is_active': 'f',
            'hide_after_due': 't',
        }
        serializer = ProctoredExamSerializer(data=data)

        self.assertFalse(serializer.is_valid())
        self.assertIn('is_proctored', serializer.errors)
        self.assertIn('is_practice_exam', serializer.errors)
Пример #6
0
 def post(self, request):
     """
     Http POST handler. Creates an exam.
     """
     serializer = ProctoredExamSerializer(data=request.DATA)
     if serializer.is_valid():
         exam_id = create_exam(
             course_id=request.DATA.get('course_id', None),
             content_id=request.DATA.get('content_id', None),
             exam_name=request.DATA.get('exam_name', None),
             time_limit_mins=request.DATA.get('time_limit_mins', None),
             is_proctored=request.DATA.get('is_proctored', None),
             is_practice_exam=request.DATA.get('is_practice_exam', None),
             external_id=request.DATA.get('external_id', None),
             is_active=request.DATA.get('is_active', None)
         )
         return Response({'exam_id': exam_id})
     else:
         return Response(
             status=status.HTTP_400_BAD_REQUEST,
             data=serializer.errors
         )
Пример #7
0
def check_for_category_switch(sender, instance, **kwargs):  # pylint: disable=unused-argument
    """
    If the exam switches from proctored to timed, notify the backend
    """
    if instance.id:
        original = sender.objects.get(pk=instance.id)
        if original.is_proctored and instance.is_proctored != original.is_proctored:
            from edx_proctoring.serializers import ProctoredExamSerializer
            exam = ProctoredExamSerializer(instance).data
            # from the perspective of the backend, the exam is now inactive.
            exam['is_active'] = False
            backend = get_backend_provider(name=exam['backend'])
            backend.on_exam_saved(exam)
Пример #8
0
 def post(self, request):
     """
     Http POST handler. Creates an exam.
     """
     serializer = ProctoredExamSerializer(data=request.data)
     if serializer.is_valid():
         exam_id = create_exam(
             course_id=request.data.get('course_id', None),
             content_id=request.data.get('content_id', None),
             exam_name=request.data.get('exam_name', None),
             time_limit_mins=request.data.get('time_limit_mins', None),
             is_proctored=request.data.get('is_proctored', None),
             is_practice_exam=request.data.get('is_practice_exam', None),
             external_id=request.data.get('external_id', None),
             is_active=request.data.get('is_active', None),
             hide_after_due=request.data.get('hide_after_due', None),
         )
         rstat = status.HTTP_200_OK
         data = {'exam_id': exam_id}
     else:
         rstat = status.HTTP_400_BAD_REQUEST
         data = serializer.errors
     return Response(status=rstat, data=data)
Пример #9
0
def get_active_exams_for_user(user_id, course_id=None):
    """
    This method will return a list of active exams for the user,
    i.e. started_at != None and completed_at == None. Theoretically there
    could be more than one, but in practice it will be one active exam.

    If course_id is set, then attempts only for an exam in that course_id
    should be returned.

    The return set should be a list of dictionary objects which are nested


    [{
        'exam': <exam fields as dict>,
        'attempt': <student attempt fields as dict>,
        'allowances': <student allowances as dict of key/value pairs
    }, {}, ...]

    """
    result = []

    student_active_exams = ProctoredExamStudentAttempt.objects.get_active_student_attempts(
        user_id, course_id)
    for active_exam in student_active_exams:
        # convert the django orm objects
        # into the serialized form.
        exam_serialized_data = ProctoredExamSerializer(
            active_exam.proctored_exam).data
        active_exam_serialized_data = ProctoredExamStudentAttemptSerializer(
            active_exam).data
        student_allowances = ProctoredExamStudentAllowance.get_allowances_for_user(
            active_exam.proctored_exam.id, user_id)
        allowance_serialized_data = [
            ProctoredExamStudentAllowanceSerializer(allowance).data
            for allowance in student_allowances
        ]
        result.append({
            'exam': exam_serialized_data,
            'attempt': active_exam_serialized_data,
            'allowances': allowance_serialized_data
        })

    return result
Пример #10
0
def save_exam_on_backend(sender, instance, **kwargs):  # pylint: disable=unused-argument
    """
    Save the exam to the backend provider when our model changes.
    It also combines the review policy into the exam when saving to the backend
    """
    if sender == models.ProctoredExam:
        exam_obj = instance
        review_policy = models.ProctoredExamReviewPolicy.get_review_policy_for_exam(instance.id)
    else:
        exam_obj = instance.proctored_exam
        review_policy = instance
    if exam_obj.is_proctored:
        from edx_proctoring.serializers import ProctoredExamSerializer
        exam = ProctoredExamSerializer(exam_obj).data
        if review_policy:
            exam['rule_summary'] = review_policy.review_policy
        backend = get_backend_provider(exam)
        external_id = backend.on_exam_saved(exam)
        if external_id and external_id != exam_obj.external_id:
            exam_obj.external_id = external_id
            exam_obj.save()
Пример #11
0
def get_exam_by_id(exam_id):
    """
    Looks up exam by the Primary Key. Raises exception if not found.

    Returns dictionary version of the Django ORM object
    e.g.
    {
        "course_id": "edX/DemoX/Demo_Course",
        "content_id": "123",
        "external_id": "",
        "exam_name": "Midterm",
        "time_limit_mins": 90,
        "is_proctored": true,
        "is_active": true
    }
    """
    proctored_exam = ProctoredExam.get_exam_by_id(exam_id)
    if proctored_exam is None:
        raise ProctoredExamNotFoundException

    serialized_exam_object = ProctoredExamSerializer(proctored_exam)
    return serialized_exam_object.data
Пример #12
0
    def on_review_callback(self, payload):
        """
        Called when the reviewing 3rd party service posts back the results

        Documentation on the data format can be found from SoftwareSecure's
        documentation named "Reviewer Data Transfer"
        """

        # redact the videoReviewLink from the payload
        if 'videoReviewLink' in payload:
            del payload['videoReviewLink']

        log_msg = (
            'Received callback from SoftwareSecure with review data: {payload}'
            .format(payload=payload))
        log.info(log_msg)

        # what we consider the external_id is SoftwareSecure's 'ssiRecordLocator'
        external_id = payload['examMetaData']['ssiRecordLocator']

        # what we consider the attempt_code is SoftwareSecure's 'examCode'
        attempt_code = payload['examMetaData']['examCode']

        # get the SoftwareSecure status on this attempt
        review_status = payload['reviewStatus']

        bad_status = review_status not in self.passing_review_status + self.failing_review_status

        if bad_status:
            err_msg = (
                'Received unexpected reviewStatus field calue from payload. '
                'Was {review_status}.'.format(review_status=review_status))
            raise ProctoredExamBadReviewStatus(err_msg)

        # do a lookup on the attempt by examCode, and compare the
        # passed in ssiRecordLocator and make sure it matches
        # what we recorded as the external_id. We need to look in both
        # the attempt table as well as the archive table

        (attempt_obj,
         is_archived_attempt) = locate_attempt_by_attempt_code(attempt_code)
        if not attempt_obj:
            # still can't find, error out
            err_msg = ('Could not locate attempt_code: {attempt_code}'.format(
                attempt_code=attempt_code))
            raise StudentExamAttemptDoesNotExistsException(err_msg)

        # then make sure we have the right external_id
        # note that SoftwareSecure might send a case insensitive
        # ssiRecordLocator than what it returned when we registered the
        # exam
        match = (attempt_obj.external_id.lower() == external_id.lower()
                 or settings.PROCTORING_SETTINGS.get(
                     'ALLOW_CALLBACK_SIMULATION', False))
        if not match:
            err_msg = (
                'Found attempt_code {attempt_code}, but the recorded external_id did not '
                'match the ssiRecordLocator that had been recorded previously. Has {existing} '
                'but received {received}!'.format(
                    attempt_code=attempt_code,
                    existing=attempt_obj.external_id,
                    received=external_id))
            raise ProctoredExamSuspiciousLookup(err_msg)

        # do we already have a review for this attempt?!? We may not allow updates
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
            attempt_code)

        if review:
            if not constants.ALLOW_REVIEW_UPDATES:
                err_msg = (
                    'We already have a review submitted from SoftwareSecure regarding '
                    'attempt_code {attempt_code}. We do not allow for updates!'
                    .format(attempt_code=attempt_code))
                raise ProctoredExamReviewAlreadyExists(err_msg)

            # we allow updates
            warn_msg = (
                'We already have a review submitted from SoftwareSecure regarding '
                'attempt_code {attempt_code}. We have been configured to allow for '
                'updates and will continue...'.format(
                    attempt_code=attempt_code))
            log.warn(warn_msg)
        else:
            # this is first time we've received this attempt_code, so
            # make a new record in the review table
            review = ProctoredExamSoftwareSecureReview()

        review.attempt_code = attempt_code
        review.raw_data = json.dumps(payload)
        review.review_status = review_status
        review.student = attempt_obj.user
        review.exam = attempt_obj.proctored_exam
        # set reviewed_by to None because it was reviewed by our 3rd party
        # service provider, not a user in our database
        review.reviewed_by = None

        review.save()

        # go through and populate all of the specific comments
        for comment in payload.get('webCamComments', []):
            self._save_review_comment(review, comment)

        for comment in payload.get('desktopComments', []):
            self._save_review_comment(review, comment)

        # we could have gotten a review for an archived attempt
        # this should *not* cause an update in our credit
        # eligibility table
        if not is_archived_attempt:
            # update our attempt status, note we have to import api.py here because
            # api.py imports software_secure.py, so we'll get an import circular reference

            allow_rejects = not constants.REQUIRE_FAILURE_SECOND_REVIEWS

            self.on_review_saved(review, allow_rejects=allow_rejects)

        # emit an event for 'review_received'
        data = {
            'review_attempt_code': review.attempt_code,
            'review_status': review.review_status,
        }

        attempt = ProctoredExamStudentAttemptSerializer(attempt_obj).data
        exam = ProctoredExamSerializer(attempt_obj.proctored_exam).data
        emit_event(exam,
                   'review_received',
                   attempt=attempt,
                   override_data=data)

        self._create_zendesk_ticket(review, exam, attempt)