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