def test_exam_attempt_is_resumable(self): # Create an exam. proctored_exam = ProctoredExam.objects.create( course_id='a/b/c', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90 ) # Create a user and their attempt user = User.objects.create(username='******', email='*****@*****.**') ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam.id, user.id, 'test_name_resumable', 'test_attempt_code_resumable', True, False, 'test_external_id_resumable' ) filter_query = { 'user_id': user.id, 'proctored_exam_id': proctored_exam.id } attempt = ProctoredExamStudentAttempt.objects.get(**filter_query) self.assertFalse(attempt.is_resumable) # No entry in the History table on creation of the Allowance entry. attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(**filter_query) self.assertEqual(len(attempt_history), 0) # Saving as an error status attempt.is_resumable = True attempt.status = ProctoredExamStudentAttemptStatus.error attempt.save() attempt = ProctoredExamStudentAttempt.objects.get(**filter_query) self.assertTrue(attempt.is_resumable) attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(**filter_query) self.assertEqual(len(attempt_history), 1) self.assertFalse(attempt_history.first().is_resumable) # Saving as a Reviewed status, but not changing is_resumable attempt.status = ProctoredExamStudentAttemptStatus.verified attempt.save() attempt = ProctoredExamStudentAttempt.objects.get(**filter_query) self.assertTrue(attempt.is_resumable) attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(**filter_query) self.assertEqual(len(attempt_history), 2) self.assertTrue(attempt_history.last().is_resumable)
def test_get_student_exam_attempt_features(self): query_features = [ 'email', 'exam_name', 'allowed_time_limit_mins', 'is_sample_attempt', 'started_at', 'completed_at', 'status', 'Suspicious Count', 'Suspicious Comments', 'Rules Violation Count', 'Rules Violation Comments', 'track' ] proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1) ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[0].id, 'Test Code 1', True, False, 'ad13') ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[1].id, 'Test Code 2', True, False, 'ad13') ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[2].id, 'Test Code 3', True, False, 'asd') proctored_exam_attempts = get_proctored_exam_results( self.course_key, query_features) assert len(proctored_exam_attempts) == 3 for proctored_exam_attempt in proctored_exam_attempts: assert set(proctored_exam_attempt.keys()) == set(query_features)
def test_get_student_exam_attempt_features(self): query_features = [ 'email', 'exam_name', 'allowed_time_limit_mins', 'is_sample_attempt', 'started_at', 'completed_at', 'status', 'Suspicious Count', 'Suspicious Comments', 'Rules Violation Count', 'Rules Violation Comments', ] proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1) ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[0].id, '', 'Test Code 1', True, False, 'ad13' ) ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[1].id, '', 'Test Code 2', True, False, 'ad13' ) ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[2].id, '', 'Test Code 3', True, False, 'asd' ) proctored_exam_attempts = get_proctored_exam_results(self.course_key, query_features) self.assertEqual(len(proctored_exam_attempts), 3) for proctored_exam_attempt in proctored_exam_attempts: self.assertEqual(set(proctored_exam_attempt.keys()), set(query_features))
def test_get_student_exam_attempt_features(self): query_features = [ 'user_email', 'exam_name', 'allowed_time_limit_mins', 'is_sample_attempt', 'started_at', 'completed_at', 'status', ] proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1) ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[0].id, '', 1, 'Test Code 1', True, False, 'ad13' ) ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[1].id, '', 2, 'Test Code 2', True, False, 'ad13' ) ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam_id, self.users[2].id, '', 3, 'Test Code 3', True, False, 'asd' ) proctored_exam_attempts = get_proctored_exam_results(self.course_key, query_features) self.assertEqual(len(proctored_exam_attempts), 3) for proctored_exam_attempt in proctored_exam_attempts: self.assertEqual(set(proctored_exam_attempt.keys()), set(query_features))
def test_get_exam_attempts(self): """ Test to get all the exam attempts for a course """ # Create an exam. proctored_exam = ProctoredExam.objects.create( course_id='a/b/c', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90) # create number of exam attempts for i in range(90): ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam.id, i, 'test_name{0}'.format(i), 'test_attempt_code{0}'.format(i), True, False, 'test_external_id{0}'.format(i)) with self.assertNumQueries(1): exam_attempts = ProctoredExamStudentAttempt.objects.get_all_exam_attempts( 'a/b/c') self.assertEqual(len(exam_attempts), 90)
def test_get_exam_attempts(self): """ Test to get all the exam attempts for a course """ # Create an exam. proctored_exam = ProctoredExam.objects.create( course_id='a/b/c', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90 ) # create number of exam attempts for i in range(90): ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam.id, i, 'test_name{0}'.format(i), i + 1, 'test_attempt_code{0}'.format(i), True, False, 'test_external_id{0}'.format(i) ) with self.assertNumQueries(1): exam_attempts = ProctoredExamStudentAttempt.objects.get_all_exam_attempts('a/b/c') self.assertEqual(len(exam_attempts), 90)
def test_exam_review_policy(self): """ Assert correct behavior of the Exam Policy model including archiving of updates and deletes """ # Create an exam. proctored_exam = ProctoredExam.objects.create( course_id='a/b/c', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90) policy = ProctoredExamReviewPolicy.objects.create( set_by_user_id=self.user.id, proctored_exam=proctored_exam, review_policy='Foo Policy', ) attempt = ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam.id, self.user.id, 'test_name{0}'.format(self.user.id), 'test_attempt_code{0}'.format(self.user.id), True, False, 'test_external_id{0}'.format(self.user.id)) attempt.review_policy_id = policy.id attempt.save() history = ProctoredExamReviewPolicyHistory.objects.all() self.assertEqual(len(history), 0) # now update it policy.review_policy = 'Updated Foo Policy' policy.save() # look in history history = ProctoredExamReviewPolicyHistory.objects.all() self.assertEqual(len(history), 1) previous = history[0] self.assertEqual(previous.set_by_user_id, self.user.id) self.assertEqual(previous.proctored_exam_id, proctored_exam.id) self.assertEqual(previous.original_id, policy.id) self.assertEqual(previous.review_policy, 'Foo Policy') # now delete updated one deleted_id = policy.id policy.delete() # look in history history = ProctoredExamReviewPolicyHistory.objects.all() self.assertEqual(len(history), 2) previous = history[0] self.assertEqual(previous.set_by_user_id, self.user.id) self.assertEqual(previous.proctored_exam_id, proctored_exam.id) self.assertEqual(previous.original_id, deleted_id) self.assertEqual(previous.review_policy, 'Foo Policy') previous = history[1] self.assertEqual(previous.set_by_user_id, self.user.id) self.assertEqual(previous.proctored_exam_id, proctored_exam.id) self.assertEqual(previous.original_id, deleted_id) self.assertEqual(previous.review_policy, 'Updated Foo Policy') # assert that we cannot delete history! with self.assertRaises(NotImplementedError): previous.delete() # now delete attempt, to make sure we preserve the policy_id in the archive table attempt.delete() attempts = ProctoredExamStudentAttemptHistory.objects.all() self.assertEqual(len(attempts), 1) self.assertEqual(attempts[0].review_policy_id, deleted_id)
def test_exam_review_policy(self): """ Assert correct behavior of the Exam Policy model including archiving of updates and deletes """ # Create an exam. proctored_exam = ProctoredExam.objects.create( course_id='a/b/c', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90 ) policy = ProctoredExamReviewPolicy.objects.create( set_by_user_id=self.user.id, proctored_exam=proctored_exam, review_policy='Foo Policy' ) attempt = ProctoredExamStudentAttempt.create_exam_attempt( proctored_exam.id, self.user.id, 'test_name{0}'.format(self.user.id), self.user.id + 1, 'test_attempt_code{0}'.format(self.user.id), True, False, 'test_external_id{0}'.format(self.user.id) ) attempt.review_policy_id = policy.id attempt.save() history = ProctoredExamReviewPolicyHistory.objects.all() self.assertEqual(len(history), 0) # now update it policy.review_policy = 'Updated Foo Policy' policy.save() # look in history history = ProctoredExamReviewPolicyHistory.objects.all() self.assertEqual(len(history), 1) previous = history[0] self.assertEqual(previous.set_by_user_id, self.user.id) self.assertEqual(previous.proctored_exam_id, proctored_exam.id) self.assertEqual(previous.original_id, policy.id) self.assertEqual(previous.review_policy, 'Foo Policy') # now delete updated one deleted_id = policy.id policy.delete() # look in history history = ProctoredExamReviewPolicyHistory.objects.all() self.assertEqual(len(history), 2) previous = history[0] self.assertEqual(previous.set_by_user_id, self.user.id) self.assertEqual(previous.proctored_exam_id, proctored_exam.id) self.assertEqual(previous.original_id, deleted_id) self.assertEqual(previous.review_policy, 'Foo Policy') previous = history[1] self.assertEqual(previous.set_by_user_id, self.user.id) self.assertEqual(previous.proctored_exam_id, proctored_exam.id) self.assertEqual(previous.original_id, deleted_id) self.assertEqual(previous.review_policy, 'Updated Foo Policy') # assert that we cannot delete history! with self.assertRaises(NotImplementedError): previous.delete() # now delete attempt, to make sure we preserve the policy_id in the archive table attempt.delete() attempts = ProctoredExamStudentAttemptHistory.objects.all() self.assertEqual(len(attempts), 1) self.assertEqual(attempts[0].review_policy_id, deleted_id)
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): """ Creates an exam attempt for user_id against exam_id. There should only be one exam_attempt per user per exam. Multiple attempts by user will be archived in a separate table """ # for now the student is allowed the exam default log_msg = ( 'Creating exam attempt for exam_id {exam_id} for ' 'user_id {user_id} with taking as proctored = {taking_as_proctored}'.format( exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored ) ) log.info(log_msg) exam = get_exam_by_id(exam_id) existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id) if existing_attempt: if existing_attempt.is_sample_attempt: # Archive the existing attempt by deleting it. existing_attempt.delete_exam_attempt() else: err_msg = ( 'Cannot create new exam attempt for exam_id = {exam_id} and ' 'user_id = {user_id} because it already exists!' ).format(exam_id=exam_id, user_id=user_id) raise StudentExamAttemptAlreadyExistsException(err_msg) allowed_time_limit_mins = exam['time_limit_mins'] # add in the allowed additional time allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id) if allowance_extra_mins: allowed_time_limit_mins += allowance_extra_mins attempt_code = unicode(uuid.uuid4()).upper() external_id = None review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id) review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(exam_id, user_id) if taking_as_proctored: scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http' callback_url = '{scheme}://{hostname}{path}'.format( scheme=scheme, hostname=settings.SITE_NAME, path=reverse( 'edx_proctoring.anonymous.proctoring_launch_callback.start_exam', args=[attempt_code] ) ) # get the name of the user, if the service is available full_name = None credit_service = get_runtime_service('credit') if credit_service: credit_state = credit_service.get_credit_state(user_id, exam['course_id']) full_name = credit_state['profile_fullname'] context = { 'time_limit_mins': allowed_time_limit_mins, 'attempt_code': attempt_code, 'is_sample_attempt': exam['is_practice_exam'], 'callback_url': callback_url, 'full_name': full_name, } # see if there is an exam review policy for this exam # if so, then pass it into the provider if review_policy: context.update({ 'review_policy': review_policy.review_policy }) # see if there is a review policy exception for this *user* # exceptions are granted on a individual basis as an # allowance if review_policy_exception: context.update({ 'review_policy_exception': review_policy_exception }) # now call into the backend provider to register exam attempt course_id = exam['course_id'] course_key = CourseKey.from_string(course_id) course = modulestore().get_course(course_key) provider_name = course.proctoring_service external_id = get_backend_provider(provider_name).register_exam_attempt( exam, context=context, ) attempt = ProctoredExamStudentAttempt.create_exam_attempt( exam_id, user_id, '', # student name is TBD allowed_time_limit_mins, attempt_code, taking_as_proctored, exam['is_practice_exam'], external_id, review_policy_id=review_policy.id if review_policy else None, ) log_msg = ( 'Created exam attempt ({attempt_id}) for exam_id {exam_id} for ' 'user_id {user_id} with taking as proctored = {taking_as_proctored} ' 'with allowed time limit minutes of {allowed_time_limit_mins}. ' 'Attempt_code {attempt_code} was generated which has a ' 'external_id of {external_id}'.format( attempt_id=attempt.id, exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored, allowed_time_limit_mins=allowed_time_limit_mins, attempt_code=attempt_code, external_id=external_id ) ) log.info(log_msg) return attempt.id
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): """ Creates an exam attempt for user_id against exam_id. There should only be one exam_attempt per user per exam. Multiple attempts by user will be archived in a separate table """ # for now the student is allowed the exam default exam = get_exam_by_id(exam_id) existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt( exam_id, user_id) if existing_attempt: log_msg = ( 'Creating exam attempt for exam_id {exam_id} for ' 'user_id {user_id} with taking as proctored = {taking_as_proctored}' .format(exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored)) log.info(log_msg) if existing_attempt.is_sample_attempt: # Archive the existing attempt by deleting it. existing_attempt.delete_exam_attempt() else: err_msg = ( 'Cannot create new exam attempt for exam_id = {exam_id} and ' 'user_id = {user_id} because it already exists!').format( exam_id=exam_id, user_id=user_id) raise StudentExamAttemptAlreadyExistsException(err_msg) allowed_time_limit_mins = exam['time_limit_mins'] # add in the allowed additional time allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted( exam_id, user_id) if allowance_extra_mins: allowed_time_limit_mins += allowance_extra_mins attempt_code = unicode(uuid.uuid4()).upper() external_id = None review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam( exam_id) review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception( exam_id, user_id) if taking_as_proctored: content_id = exam['content_id'].split('@')[-1] # get hash scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http' callback_url = '{scheme}://{hostname}{path}'.format( scheme=scheme, hostname=settings.SITE_NAME, path=reverse('jump_to_id', kwargs={ 'course_id': exam['course_id'], 'module_id': content_id })) # get the name of the user, if the service is available full_name = None credit_service = get_runtime_service('credit') user = User.objects.get(pk=user_id) context = { 'time_limit_mins': allowed_time_limit_mins, 'attempt_code': attempt_code, 'is_sample_attempt': exam['is_practice_exam'], 'callback_url': callback_url, 'user_id': user_id, 'full_name': " ".join((user.first_name, user.last_name)), 'username': user.username, 'email': user.email } # see if there is an exam review policy for this exam # if so, then pass it into the provider if review_policy: context.update({'review_policy': review_policy.review_policy}) # see if there is a review policy exception for this *user* # exceptions are granted on a individual basis as an # allowance if review_policy_exception: context.update( {'review_policy_exception': review_policy_exception}) # now call into the backend provider to register exam attempt provider_name = get_provider_name_by_course_id(exam['course_id']) external_id = get_backend_provider( provider_name).register_exam_attempt( exam, context=context, ) attempt = ProctoredExamStudentAttempt.create_exam_attempt( exam_id, user_id, '', # student name is TBD allowed_time_limit_mins, attempt_code, taking_as_proctored, exam['is_practice_exam'], external_id, review_policy_id=review_policy.id if review_policy else None, ) log_msg = ( '{attempt_code} - {username} ({email}) ' 'Created exam attempt ({attempt_id}) for exam_id {exam_id} for ' 'user_id {user_id} with taking as proctored = {taking_as_proctored} ' 'with allowed time limit minutes of {allowed_time_limit_mins}. ' 'external_id of {external_id}'.format( attempt_id=attempt.id, exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored, allowed_time_limit_mins=allowed_time_limit_mins, attempt_code=attempt_code, external_id=external_id, username=attempt.user.username, email=attempt.user.email)) log.info(log_msg) return attempt.id
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): """ Creates an exam attempt for user_id against exam_id. There should only be one exam_attempt per user per exam. Multiple attempts by user will be archived in a separate table """ # for now the student is allowed the exam default exam = get_exam_by_id(exam_id) existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id) if existing_attempt: log_msg = ( 'Creating exam attempt for exam_id {exam_id} for ' 'user_id {user_id} with taking as proctored = {taking_as_proctored}'.format( exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored ) ) log.info(log_msg) if existing_attempt.is_sample_attempt: # Archive the existing attempt by deleting it. existing_attempt.delete_exam_attempt() else: err_msg = ( 'Cannot create new exam attempt for exam_id = {exam_id} and ' 'user_id = {user_id} because it already exists!' ).format(exam_id=exam_id, user_id=user_id) raise StudentExamAttemptAlreadyExistsException(err_msg) allowed_time_limit_mins = exam['time_limit_mins'] # add in the allowed additional time allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id) if allowance_extra_mins: allowed_time_limit_mins += allowance_extra_mins attempt_code = unicode(uuid.uuid4()).upper() external_id = None review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id) review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(exam_id, user_id) if taking_as_proctored: content_id = exam['content_id'].split('@')[-1] # get hash scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http' callback_url = '{scheme}://{hostname}{path}'.format( scheme=scheme, hostname=settings.SITE_NAME, path=reverse( 'jump_to_id', kwargs={'course_id': exam['course_id'], 'module_id': content_id} ) ) # get the name of the user, if the service is available full_name = None credit_service = get_runtime_service('credit') user = User.objects.get(pk=user_id) context = { 'time_limit_mins': allowed_time_limit_mins, 'attempt_code': attempt_code, 'is_sample_attempt': exam['is_practice_exam'], 'callback_url': callback_url, 'user_id': user_id, 'full_name': " ".join((user.first_name,user.last_name)), 'username': user.username, 'email': user.email } # see if there is an exam review policy for this exam # if so, then pass it into the provider if review_policy: context.update({ 'review_policy': review_policy.review_policy }) # see if there is a review policy exception for this *user* # exceptions are granted on a individual basis as an # allowance if review_policy_exception: context.update({ 'review_policy_exception': review_policy_exception }) # now call into the backend provider to register exam attempt provider_name = get_provider_name_by_course_id(exam['course_id']) external_id = get_backend_provider(provider_name).register_exam_attempt( exam, context=context, ) attempt = ProctoredExamStudentAttempt.create_exam_attempt( exam_id, user_id, '', # student name is TBD allowed_time_limit_mins, attempt_code, taking_as_proctored, exam['is_practice_exam'], external_id, review_policy_id=review_policy.id if review_policy else None, ) log_msg = ( '{attempt_code} - {username} ({email}) ' 'Created exam attempt ({attempt_id}) for exam_id {exam_id} for ' 'user_id {user_id} with taking as proctored = {taking_as_proctored} ' 'with allowed time limit minutes of {allowed_time_limit_mins}. ' 'external_id of {external_id}'.format( attempt_id=attempt.id, exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored, allowed_time_limit_mins=allowed_time_limit_mins, attempt_code=attempt_code, external_id=external_id, username=attempt.user.username, email=attempt.user.email ) ) log.info(log_msg) return attempt.id