Commit f2c320fb by Andy Armstrong Committed by GitHub

Merge pull request #308 from edx/andya/clean-up-tests

Clean up the Python tests
parents 2356452f 1faad3ad
......@@ -53,4 +53,8 @@ coverage/
htmlcov/
acceptance_tests/*.png
node_modules/
\ No newline at end of file
node_modules/
# Devstack
edx-proctoring
edx_proctoring.egg-info
......@@ -6,7 +6,6 @@ All tests for the api.py
"""
import ddt
from datetime import datetime, timedelta
from django.core import mail
from mock import patch
import pytz
from freezegun import freeze_time
......@@ -24,7 +23,6 @@ from edx_proctoring.api import (
get_active_exams_for_user,
get_exam_attempt,
create_exam_attempt,
get_student_view,
get_allowances_for_course,
get_all_exams_for_course,
get_exam_attempt_by_id,
......@@ -61,304 +59,28 @@ from edx_proctoring.exceptions import (
from edx_proctoring.models import (
ProctoredExam,
ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
ProctoredExamReviewPolicy,
)
from .utils import (
LoggedInTestCase,
ProctoredExamTestCase,
)
from edx_proctoring.tests.test_services import (
MockCreditService, MockCreditServiceWithCourseEndDate,
MockInstructorService, MockCreditServiceNone,
from .test_services import (
MockCreditService,
MockCreditServiceNone,
MockCreditServiceWithCourseEndDate,
)
from edx_proctoring.runtime import set_runtime_service, get_runtime_service
from eventtracking import tracker
from eventtracking.tracker import Tracker, TRACKERS
class MockTracker(Tracker):
"""
A mocked out tracker which implements the emit method
"""
def emit(self, name=None, data=None):
"""
Overload this method to do nothing
"""
pass
@ddt.ddt
class ProctoredExamApiTests(LoggedInTestCase):
class ProctoredExamApiTests(ProctoredExamTestCase):
"""
All tests for the models.py
"""
def setUp(self):
"""
Build out test harnessing
"""
super(ProctoredExamApiTests, self).setUp()
self.default_time_limit = 21
self.course_id = 'test_course'
self.content_id_for_exam_with_due_date = 'test_content_due_date_id'
self.content_id = 'test_content_id'
self.content_id_timed = 'test_content_id_timed'
self.content_id_practice = 'test_content_id_practice'
self.disabled_content_id = 'test_disabled_content_id'
self.exam_name = 'Test Exam'
self.user_id = self.user.id
self.key = 'additional_time_granted'
self.value = '10'
self.external_id = 'test_external_id'
self.proctored_exam_id = self._create_proctored_exam()
self.timed_exam_id = self._create_timed_exam()
self.practice_exam_id = self._create_practice_exam()
self.disabled_exam_id = self._create_disabled_exam()
# Messages for get_student_view
self.start_an_exam_msg = 'This exam is proctored'
self.exam_expired_msg = 'The due date for this exam has passed'
self.timed_exam_msg = '{exam_name} is a Timed Exam'
self.timed_exam_submitted = 'You have submitted your timed exam.'
self.timed_exam_expired = 'The time allotted for this exam has expired.'
self.submitted_timed_exam_msg_with_due_date = 'After the due date has passed,'
self.exam_time_expired_msg = 'You did not complete the exam in the allotted time'
self.exam_time_error_msg = 'A technical error has occurred with your proctored exam'
self.chose_proctored_exam_msg = 'Follow these steps to set up and start your proctored exam'
self.proctored_exam_optout_msg = 'Take this exam as an open exam instead'
self.proctored_exam_completed_msg = 'Are you sure you want to end your proctored exam'
self.proctored_exam_waiting_for_app_shutdown_msg = 'You are about to complete your proctored exam'
self.proctored_exam_submitted_msg = 'You have submitted this proctored exam for review'
self.proctored_exam_verified_msg = 'Your proctoring session was reviewed and passed all requirements'
self.proctored_exam_rejected_msg = 'Your proctoring session was reviewed and did not pass requirements'
self.start_a_practice_exam_msg = 'Get familiar with proctoring for real exams later in the course'
self.practice_exam_submitted_msg = 'You have submitted this practice proctored exam'
self.practice_exam_created_msg = 'Follow these steps to set up and start your proctored exam'
self.practice_exam_completion_msg = 'Are you sure you want to end your proctored exam'
self.ready_to_start_msg = 'Follow these instructions'
self.practice_exam_failed_msg = 'There was a problem with your practice proctoring session'
self.proctored_exam_email_subject = 'Proctoring Session Results Update'
self.proctored_exam_email_body = 'the status of your proctoring session review'
self.footer_msg = 'About Proctored Exams'
self.timed_footer_msg = 'Can I request additional time to complete my exam?'
set_runtime_service('credit', MockCreditService())
set_runtime_service('instructor', MockInstructorService(is_user_course_staff=True))
tracker.register_tracker(MockTracker())
self.prerequisites = [
{
'namespace': 'proctoring',
'name': 'proc1',
'order': 2,
'status': 'satisfied',
},
{
'namespace': 'reverification',
'name': 'rever1',
'order': 1,
'status': 'satisfied',
},
{
'namespace': 'grade',
'name': 'grade1',
'order': 0,
'status': 'pending',
},
{
'namespace': 'reverification',
'name': 'rever2',
'order': 3,
'status': 'failed',
},
{
'namespace': 'proctoring',
'name': 'proc2',
'order': 4,
'status': 'pending',
},
]
self.declined_prerequisites = [
{
'namespace': 'proctoring',
'name': 'proc1',
'order': 2,
'status': 'satisfied',
},
{
'namespace': 'reverification',
'name': 'rever1',
'order': 1,
'status': 'satisfied',
},
{
'namespace': 'grade',
'name': 'grade1',
'order': 0,
'status': 'pending',
},
{
'namespace': 'reverification',
'name': 'rever2',
'order': 3,
'status': 'declined',
},
{
'namespace': 'proctoring',
'name': 'proc2',
'order': 4,
'status': 'pending',
},
]
def tearDown(self):
"""
Cleanup
"""
del TRACKERS['default']
def _create_proctored_exam(self):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit
)
def _create_exam_with_due_time(self, is_proctored=True, is_practice_exam=False, due_date=None):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_proctored=is_proctored,
is_practice_exam=is_practice_exam,
due_date=due_date
)
def _create_timed_exam(self):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_timed,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_proctored=False
)
def _create_practice_exam(self):
"""
Calls the api's create_exam to create a practice exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_practice,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_practice_exam=True,
is_proctored=True
)
def _create_disabled_exam(self):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
is_proctored=False,
content_id=self.disabled_content_id,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_active=False
)
def _create_exam_attempt(self, exam_id, status='created'):
"""
Creates the ProctoredExamStudentAttempt object.
"""
attempt = ProctoredExamStudentAttempt(
proctored_exam_id=exam_id,
user_id=self.user_id,
external_id=self.external_id,
allowed_time_limit_mins=10,
status=status
)
if status in (ProctoredExamStudentAttemptStatus.started,
ProctoredExamStudentAttemptStatus.ready_to_submit, ProctoredExamStudentAttemptStatus.submitted):
attempt.started_at = datetime.now(pytz.UTC)
if ProctoredExamStudentAttemptStatus.is_completed_status(status):
attempt.completed_at = datetime.now(pytz.UTC)
attempt.save()
return attempt
def _create_unstarted_exam_attempt(self, is_proctored=True, is_practice=False):
"""
Creates the ProctoredExamStudentAttempt object.
"""
if is_proctored:
if is_practice:
exam_id = self.practice_exam_id
else:
exam_id = self.proctored_exam_id
else:
exam_id = self.timed_exam_id
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=exam_id,
user_id=self.user_id,
external_id=self.external_id,
allowed_time_limit_mins=10,
status='created'
)
def _create_started_exam_attempt(self, started_at=None, is_proctored=True, is_sample_attempt=False):
"""
Creates the ProctoredExamStudentAttempt object.
"""
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id if is_proctored else self.timed_exam_id,
user_id=self.user_id,
external_id=self.external_id,
started_at=started_at if started_at else datetime.now(pytz.UTC),
status=ProctoredExamStudentAttemptStatus.started,
allowed_time_limit_mins=10,
taking_as_proctored=is_proctored,
is_sample_attempt=is_sample_attempt
)
def _create_started_practice_exam_attempt(self, started_at=None):
"""
Creates the ProctoredExamStudentAttempt object.
"""
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.practice_exam_id,
taking_as_proctored=True,
user_id=self.user_id,
external_id=self.external_id,
started_at=started_at if started_at else datetime.now(pytz.UTC),
is_sample_attempt=True,
status=ProctoredExamStudentAttemptStatus.started,
allowed_time_limit_mins=10
)
def _add_allowance_for_user(self):
"""
creates allowance for user.
......@@ -1023,215 +745,6 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertEqual(all_exams[0]['id'], updated_exam_attempt_id)
self.assertEqual(all_exams[1]['id'], exam_attempt.id)
def test_get_student_view(self):
"""
Test for get_student_view prompting the user to take the exam
as a timed exam or a proctored exam.
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'hide_after_due': False,
}
)
self.assertIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
# try practice exam variant
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id + 'foo',
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'is_practice_exam': True,
'hide_after_due': False,
}
)
self.assertIn(self.start_a_practice_exam_msg.format(exam_name=self.exam_name), rendered_response)
def test_get_honor_view_with_practice_exam(self):
"""
Test for get_student_view prompting when the student is enrolled in non-verified
track for a practice exam, this should return not None, meaning
student will see proctored content
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'honor'
},
'is_practice_exam': True
}
)
self.assertIsNotNone(rendered_response)
def test_get_honor_view(self):
"""
Test for get_student_view prompting when the student is enrolled in non-verified
track, this should return None
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'honor'
},
'is_practice_exam': False
}
)
self.assertIsNone(rendered_response)
@ddt.data(
('reverification', None, 'The following prerequisites are in a <strong>pending</strong> state', True),
('reverification', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('reverification', 'failed', 'You did not satisfy the following prerequisites', True),
('reverification', 'satisfied', 'To be eligible to earn credit for this course', False),
('reverification', 'declined', None, False),
('proctored_exam', None, 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'failed', 'You did not satisfy the following prerequisites', True),
('proctored_exam', 'satisfied', 'To be eligible to earn credit for this course', False),
('proctored_exam', 'declined', None, False),
('grade', 'failed', 'To be eligible to earn credit for this course', False),
# this is nonesense, but let's double check it
('grade', 'declined', 'To be eligible to earn credit for this course', False),
)
@ddt.unpack
def test_prereq_scenarios(self, namespace, req_status, expected_content, should_see_prereq):
"""
This test asserts that proctoring will not be displayed under the following
conditions:
- Verified student has not completed all 'reverification' requirements
"""
exam = get_exam_by_id(self.proctored_exam_id)
# user hasn't attempted reverifications
rendered_response = get_student_view(
user_id=self.user_id,
course_id=exam['course_id'],
content_id=exam['content_id'],
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'is_practice_exam': False,
'credit_state': {
'enrollment_mode': 'verified',
'credit_requirement_status': [
{
'namespace': namespace,
'name': 'foo',
'display_name': 'Foo Requirement',
'status': req_status,
'order': 0
}
]
}
}
)
if expected_content:
self.assertIn(expected_content, rendered_response)
else:
self.assertIsNone(rendered_response)
if req_status == 'declined' and not expected_content:
# also we should have auto-declined if a pre-requisite was declined
attempt = get_exam_attempt(exam['id'], self.user_id)
self.assertIsNotNone(attempt)
self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.declined)
if should_see_prereq:
self.assertIn('Foo Requirement', rendered_response)
def test_student_view_non_student(self):
"""
Make sure that if we ask for a student view if we are not in a student role,
then we don't see any proctoring views
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
},
user_role='staff'
)
self.assertIsNone(rendered_response)
def test_wrong_exam_combo(self):
"""
Verify that we get a None back when rendering a view
for a practice, non-proctored exam. This is unsupported.
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id='foo',
content_id='bar',
context={
'is_proctored': False,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'hide_after_due': False,
},
user_role='student'
)
self.assertIsNone(rendered_response)
def test_proctored_exam_passed_end_date(self):
"""
Verify that we get a None back on a proctored exam
if the course end date is passed
"""
set_runtime_service('credit', MockCreditServiceWithCourseEndDate())
rendered_response = get_student_view(
user_id=self.user_id,
course_id='foo',
content_id='bar',
context={
'is_proctored': True,
'is_practice_exam': False,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'due_date': None,
'hide_after_due': False,
},
user_role='student'
)
self.assertIsNone(rendered_response)
def test_proctored_status_summary_passed_end_date(self):
"""
Assert that we get the expected status summaries
......@@ -1250,923 +763,89 @@ class ProctoredExamApiTests(LoggedInTestCase):
}
self.assertIn(summary, [expected])
def test_practice_exam_passed_end_date(self):
def test_submitted_credit_state(self):
"""
Verify that we get a None back on a practice exam
if the course end date is passed
Verify that putting an attempt into the submitted state will also mark
the credit requirement as submitted
"""
set_runtime_service('credit', MockCreditServiceWithCourseEndDate())
rendered_response = get_student_view(
user_id=self.user_id,
course_id='foo',
content_id='bar',
context={
'is_proctored': True,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'due_date': None,
'hide_after_due': False,
},
user_role='student'
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
ProctoredExamStudentAttemptStatus.submitted
)
self.assertIsNone(rendered_response)
def test_get_disabled_student_view(self):
"""
Assert that a disabled proctored exam will not override the
student_view
"""
self.assertIsNone(
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.disabled_content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
credit_service = get_runtime_service('credit')
credit_status = credit_service.get_credit_state(self.user.id, exam_attempt.proctored_exam.course_id)
self.assertEqual(len(credit_status['credit_requirement_status']), 1)
self.assertEqual(
credit_status['credit_requirement_status'][0]['status'],
'submitted'
)
def test_get_studentview_unstarted_exam(self):
def test_error_credit_state(self):
"""
Test for get_student_view proctored exam which has not started yet.
Verify that putting an attempt into the error state will also mark
the credit requirement as failed
"""
self._create_unstarted_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertIn(self.proctored_exam_optout_msg, rendered_response)
# now make sure content remains the same if
# the status transitions to 'download_software_clicked'
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
self.proctored_exam_id,
self.user_id,
ProctoredExamStudentAttemptStatus.download_software_clicked
exam_attempt.proctored_exam_id,
self.user.id,
ProctoredExamStudentAttemptStatus.error
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
credit_service = get_runtime_service('credit')
credit_status = credit_service.get_credit_state(self.user.id, exam_attempt.proctored_exam.course_id)
self.assertEqual(len(credit_status['credit_requirement_status']), 1)
self.assertEqual(
credit_status['credit_requirement_status'][0]['status'],
'failed'
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertIn(self.proctored_exam_optout_msg, rendered_response)
def test_get_studentview_unstarted_practice_exam(self):
@ddt.data(
(
ProctoredExamStudentAttemptStatus.declined,
False,
None,
ProctoredExamStudentAttemptStatus.declined
),
(
ProctoredExamStudentAttemptStatus.rejected,
False,
None,
None
),
(
ProctoredExamStudentAttemptStatus.rejected,
True,
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.created
),
(
ProctoredExamStudentAttemptStatus.rejected,
True,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.verified
),
(
ProctoredExamStudentAttemptStatus.declined,
True,
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.submitted
),
)
@ddt.unpack
def test_cascading(self, to_status, create_attempt, second_attempt_status, expected_second_status):
"""
Test for get_student_view Practice exam which has not started yet.
Make sure that when we decline/reject one attempt all other exams in the course
are auto marked as declined
"""
self._create_unstarted_exam_attempt(is_practice=True)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'is_practice_exam': True,
'default_time_limit_mins': 90
}
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertNotIn(self.proctored_exam_optout_msg, rendered_response)
def test_declined_attempt(self):
"""
Make sure that a declined attempt does not show proctoring
"""
attempt_obj = self._create_unstarted_exam_attempt()
attempt_obj.status = ProctoredExamStudentAttemptStatus.declined
attempt_obj.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
def test_get_studentview_ready(self):
"""
Assert that we get the right content
when the exam is ready to be started
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_start
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.ready_to_start_msg, rendered_response)
def test_get_studentview_started_exam(self):
"""
Test for get_student_view proctored exam which has started.
"""
self._create_started_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
def test_get_studentview_started_practice_exam(self):
"""
Test for get_student_view practice proctored exam which has started.
"""
self._create_started_practice_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
def test_get_studentview_started_timed_exam(self):
"""
Test for get_student_view timed exam which has started.
"""
self._create_started_exam_attempt(is_proctored=False)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
@ddt.data(True, False)
def test_get_studentview_long_limit(self, under_exception):
"""
Test for hide_extra_time_footer on exams with > 20 hours time limit
"""
exam_id = self._create_exam_with_due_time(is_proctored=False, )
if under_exception:
update_exam(exam_id, time_limit_mins=((20 * 60))) # exactly 20 hours
else:
update_exam(exam_id, time_limit_mins=((20 * 60) + 1)) # 1 minute greater than 20 hours
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': False,
'display_name': self.exam_name,
}
)
if under_exception:
self.assertIn(self.timed_footer_msg, rendered_response)
else:
self.assertNotIn(self.timed_footer_msg, rendered_response)
@ddt.data(
(datetime.now(pytz.UTC) + timedelta(days=1), False),
(datetime.now(pytz.UTC) - timedelta(days=1), False),
(datetime.now(pytz.UTC) - timedelta(days=1), True),
)
@ddt.unpack
def test_get_studentview_submitted_timed_exam_with_past_due_date(self, due_date, hide_after_due):
"""
Test for get_student_view timed exam with the due date.
"""
# exam is created with due datetime which has already passed
exam_id = self._create_exam_with_due_time(is_proctored=False, due_date=due_date)
if hide_after_due:
update_exam(exam_id, hide_after_due=hide_after_due)
# now create the timed_exam attempt in the submitted state
self._create_exam_attempt(exam_id, status='submitted')
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 10,
'due_date': due_date,
}
)
if datetime.now(pytz.UTC) < due_date:
self.assertIn(self.timed_exam_submitted, rendered_response)
self.assertIn(self.submitted_timed_exam_msg_with_due_date, rendered_response)
elif hide_after_due:
self.assertIn(self.timed_exam_submitted, rendered_response)
self.assertNotIn(self.submitted_timed_exam_msg_with_due_date, rendered_response)
else:
self.assertIsNone(rendered_response)
def test_proctored_exam_attempt_with_past_due_datetime(self):
"""
Test for get_student_view for proctored exam with past due datetime
"""
due_date = datetime.now(pytz.UTC) + timedelta(days=1)
# exam is created with due datetime which has already passed
self._create_exam_with_due_time(due_date=due_date)
# due_date is exactly after 24 hours, if student arrives after 2 days
# then he can not attempt the proctored exam
reset_time = due_date + timedelta(days=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
# call the view again, because the first call set the exam attempt to 'expired'
# this second call will render the view based on the state
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': True,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
def test_timed_exam_attempt_with_past_due_datetime(self):
"""
Test for get_student_view for timed exam with past due datetime
"""
due_date = datetime.now(pytz.UTC) + timedelta(days=1)
# exam is created with due datetime which has already passed
self._create_exam_with_due_time(
due_date=due_date,
is_proctored=False
)
# due_date is exactly after 24 hours, if student arrives after 2 days
# then he can not attempt the proctored exam
reset_time = due_date + timedelta(days=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
# call the view again, because the first call set the exam attempt to 'expired'
# this second call will render the view based on the state
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': True,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_get_studentview_timedout(self):
"""
Verifies that if we call get_studentview when the timer has expired
it will automatically state transition into timed_out
"""
self._create_started_exam_attempt()
reset_time = datetime.now(pytz.UTC) + timedelta(days=1)
with freeze_time(reset_time):
with self.assertRaises(NotImplementedError):
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
def test_get_studentview_submitted_status(self):
"""
Test for get_student_view proctored exam which has been submitted.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.submitted
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_waiting_for_app_shutdown_msg, rendered_response)
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
# now make sure if this status transitions to 'second_review_required'
# the student will still see a 'submitted' message
update_attempt_status(
exam_attempt.proctored_exam_id,
exam_attempt.user_id,
ProctoredExamStudentAttemptStatus.second_review_required
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
def test_get_studentview_submitted_status_with_duedate(self):
"""
Test for get_student_view proctored exam which has been submitted
And due date has passed
"""
proctored_exam = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=30,
is_proctored=True,
is_active=True,
due_date=datetime.now(pytz.UTC) + timedelta(minutes=40)
)
exam_attempt = ProctoredExamStudentAttempt.objects.create(
proctored_exam=proctored_exam,
user=self.user,
allowed_time_limit_mins=30,
taking_as_proctored=True,
external_id=proctored_exam.external_id,
status=ProctoredExamStudentAttemptStatus.submitted,
last_poll_timestamp=datetime.now(pytz.UTC)
)
# due date is after 10 minutes
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=20)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user.id,
course_id='a/b/c',
content_id='test_content',
context={
'is_proctored': True,
'display_name': 'Test Exam',
'default_time_limit_mins': 30
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
exam_attempt.is_status_acknowledged = True
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user.id,
course_id='a/b/c',
content_id='test_content',
context={
'is_proctored': True,
'display_name': 'Test Exam',
'default_time_limit_mins': 30
}
)
self.assertIsNotNone(rendered_response)
def test_get_studentview_submitted_status_practiceexam(self):
"""
Test for get_student_view practice exam which has been submitted.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.submitted
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_waiting_for_app_shutdown_msg, rendered_response)
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_submitted_msg, rendered_response)
@ddt.data(
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
)
def test_get_studentview_created_status_practiceexam(self, status):
"""
Test for get_student_view practice exam which has been created.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = status
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_created_msg, rendered_response)
def test_get_studentview_ready_to_start_status_practiceexam(self):
"""
Test for get_student_view practice exam which is ready to start.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_start
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.ready_to_start_msg, rendered_response)
def test_get_studentview_compelete_status_practiceexam(self):
"""
Test for get_student_view practice exam when it is complete/ready to submit.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_submit
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_completion_msg, rendered_response)
def test_get_studentview_rejected_status(self):
"""
Test for get_student_view proctored exam which has been rejected.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.rejected
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_rejected_msg, rendered_response)
def test_get_studentview_verified_status(self):
"""
Test for get_student_view proctored exam which has been verified.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.verified
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_verified_msg, rendered_response)
def test_get_studentview_completed_status(self):
"""
Test for get_student_view proctored exam which has been completed.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_submit
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_completed_msg, rendered_response)
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_get_studentview_expired(self):
"""
Test for get_student_view proctored exam which has expired. Since we don't have a template
for that view rendering, it will throw a NotImplementedError
"""
self._create_started_exam_attempt(started_at=datetime.now(pytz.UTC).replace(year=2010))
with self.assertRaises(NotImplementedError):
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
def test_get_studentview_erroneous_exam(self):
"""
Test for get_student_view proctored exam which has exam status error.
"""
ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id,
user_id=self.user_id,
external_id=self.external_id,
started_at=datetime.now(pytz.UTC),
allowed_time_limit_mins=10,
status='error'
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 10
}
)
self.assertIn(self.exam_time_error_msg, rendered_response)
def test_get_studentview_erroneous_practice_exam(self):
"""
Test for get_student_view practice exam which has exam status error.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.error
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_failed_msg, rendered_response)
def test_get_studentview_unstarted_timed_exam(self):
"""
Test for get_student_view Timed exam which is not proctored and has not started yet.
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id="abc",
content_id=self.content_id,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'hide_after_due': False,
}
)
self.assertNotIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response)
self.assertIn('1 hour and 30 minutes', rendered_response)
self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
def test_get_studentview_unstarted_timed_exam_with_allowance(self):
"""
Test for get_student_view Timed exam which is not proctored and has not started yet.
But user has an allowance
"""
allowed_extra_time = 10
add_allowance_for_user(
self.timed_exam_id,
self.user.username,
ProctoredExamStudentAllowance.ADDITIONAL_TIME_GRANTED,
str(allowed_extra_time)
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={}
)
self.assertNotIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response)
self.assertIn('31 minutes', rendered_response)
self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
@ddt.data(
(
ProctoredExamStudentAttemptStatus.ready_to_submit,
'Are you sure that you want to submit your timed exam?'
),
(
ProctoredExamStudentAttemptStatus.submitted,
'You have submitted your timed exam'
),
)
@ddt.unpack
def test_get_studentview_completed_timed_exam(self, status, expected_content):
"""
Test for get_student_view timed exam which has completed.
"""
exam_attempt = self._create_started_exam_attempt(is_proctored=False)
exam_attempt.status = status
if status == 'submitted':
exam_attempt.completed_at = datetime.now(pytz.UTC)
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'display_name': self.exam_name,
}
)
self.assertIn(expected_content, rendered_response)
def test_expired_exam(self):
"""
Test that an expired exam shows a difference message when the exam is expired just recently
"""
# create exam with completed_at equal to current time and started_at equal to allowed_time_limit_mins ago
attempt = self._create_started_exam_attempt(is_proctored=False)
attempt.status = "submitted"
attempt.started_at = attempt.started_at - timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.completed_at = attempt.started_at + timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'display_name': self.exam_name,
}
)
self.assertIn(self.timed_exam_expired, rendered_response)
# update start and completed time such that completed_time is allowed_time_limit_mins ago than the current time
attempt.started_at = attempt.started_at - timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.completed_at = attempt.completed_at - timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'display_name': self.exam_name,
}
)
self.assertIn(self.timed_exam_submitted, rendered_response)
def test_submitted_credit_state(self):
"""
Verify that putting an attempt into the submitted state will also mark
the credit requirement as submitted
"""
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
ProctoredExamStudentAttemptStatus.submitted
)
credit_service = get_runtime_service('credit')
credit_status = credit_service.get_credit_state(self.user.id, exam_attempt.proctored_exam.course_id)
self.assertEqual(len(credit_status['credit_requirement_status']), 1)
self.assertEqual(
credit_status['credit_requirement_status'][0]['status'],
'submitted'
)
def test_error_credit_state(self):
"""
Verify that putting an attempt into the error state will also mark
the credit requirement as failed
"""
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
ProctoredExamStudentAttemptStatus.error
)
credit_service = get_runtime_service('credit')
credit_status = credit_service.get_credit_state(self.user.id, exam_attempt.proctored_exam.course_id)
self.assertEqual(len(credit_status['credit_requirement_status']), 1)
self.assertEqual(
credit_status['credit_requirement_status'][0]['status'],
'failed'
)
@ddt.data(
(
ProctoredExamStudentAttemptStatus.declined,
False,
None,
ProctoredExamStudentAttemptStatus.declined
),
(
ProctoredExamStudentAttemptStatus.rejected,
False,
None,
None
),
(
ProctoredExamStudentAttemptStatus.rejected,
True,
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.created
),
(
ProctoredExamStudentAttemptStatus.rejected,
True,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.verified
),
(
ProctoredExamStudentAttemptStatus.declined,
True,
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.submitted
),
)
@ddt.unpack
def test_cascading(self, to_status, create_attempt, second_attempt_status, expected_second_status):
"""
Make sure that when we decline/reject one attempt all other exams in the course
are auto marked as declined
"""
# create other exams in course
second_exam_id = create_exam(
# create other exams in course
second_exam_id = create_exam(
course_id=self.course_id,
content_id="2nd exam",
exam_name="2nd exam",
......@@ -2656,183 +1335,6 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertEquals(attempt['last_poll_timestamp'], now)
self.assertEquals(attempt['last_poll_ipaddr'], '1.1.1.1')
@ddt.data(
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected
)
def test_send_email(self, status):
"""
Assert that email is sent on the following statuses of proctoring attempt.
"""
exam_attempt = self._create_started_exam_attempt()
credit_state = get_runtime_service('credit').get_credit_state(self.user_id, self.course_id)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 1)
self.assertIn(self.proctored_exam_email_subject, mail.outbox[0].subject)
self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body)
self.assertIn(ProctoredExamStudentAttemptStatus.get_status_alias(status), mail.outbox[0].body)
self.assertIn(credit_state['course_name'], mail.outbox[0].body)
@ddt.data(
ProctoredExamStudentAttemptStatus.second_review_required,
ProctoredExamStudentAttemptStatus.error
)
def test_email_not_sent(self, status):
"""
Assert than email is not sent on the following statuses of proctoring attempt
"""
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
def test_send_email_unicode(self):
"""
Assert that email can be sent with a unicode course name.
"""
course_name = u'अआईउऊऋऌ अआईउऊऋऌ'
set_runtime_service('credit', MockCreditService(course_name=course_name))
exam_attempt = self._create_started_exam_attempt()
credit_state = get_runtime_service('credit').get_credit_state(self.user_id, self.course_id)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
ProctoredExamStudentAttemptStatus.submitted
)
self.assertEquals(len(mail.outbox), 1)
self.assertIn(self.proctored_exam_email_subject, mail.outbox[0].subject)
self.assertIn(course_name, mail.outbox[0].subject)
self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body)
self.assertIn(
ProctoredExamStudentAttemptStatus.get_status_alias(
ProctoredExamStudentAttemptStatus.submitted
),
mail.outbox[0].body
)
self.assertIn(credit_state['course_name'], mail.outbox[0].body)
@ddt.data(
ProctoredExamStudentAttemptStatus.eligible,
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
ProctoredExamStudentAttemptStatus.ready_to_start,
ProctoredExamStudentAttemptStatus.started,
ProctoredExamStudentAttemptStatus.ready_to_submit,
ProctoredExamStudentAttemptStatus.declined,
ProctoredExamStudentAttemptStatus.timed_out,
ProctoredExamStudentAttemptStatus.error
)
@patch.dict('settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_not_send_email(self, status):
"""
Assert that email is not sent on the following statuses of proctoring attempt.
"""
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
@ddt.data(
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected
)
def test_not_send_email_sample_exam(self, status):
"""
Assert that email is not sent when there is practice/sample exam
"""
exam_attempt = self._create_started_exam_attempt(is_sample_attempt=True)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
@ddt.data(
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected
)
def test_not_send_email_timed_exam(self, status):
"""
Assert that email is not sent when exam is timed/not-proctoring
"""
exam_attempt = self._create_started_exam_attempt(is_proctored=False)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
@ddt.data(
ProctoredExamStudentAttemptStatus.eligible,
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected,
)
def test_footer_present(self, status):
"""
Make sure the footer content is visible in the rendered output
"""
if status != ProctoredExamStudentAttemptStatus.eligible:
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = status
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNotNone(rendered_response)
if status == ProctoredExamStudentAttemptStatus.submitted:
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.footer_msg, rendered_response)
else:
self.assertIn(self.footer_msg, rendered_response)
def test_requirement_status_order(self):
"""
Make sure that we get a correct ordered list of all statuses sorted in the correct
......
# coding=utf-8
"""
All tests for proctored exam emails.
"""
import ddt
from django.core import mail
from mock import patch
from edx_proctoring.api import (
update_attempt_status,
)
from edx_proctoring.models import (
ProctoredExamStudentAttemptStatus,
)
from edx_proctoring.runtime import set_runtime_service, get_runtime_service
from .test_services import (
MockCreditService,
)
from .utils import (
ProctoredExamTestCase,
)
@ddt.ddt
class ProctoredExamEmailTests(ProctoredExamTestCase):
"""
All tests for proctored exam emails.
"""
def setUp(self):
"""
Build out test harnessing
"""
super(ProctoredExamEmailTests, self).setUp()
# Messages for get_student_view
self.proctored_exam_email_subject = 'Proctoring Session Results Update'
self.proctored_exam_email_body = 'the status of your proctoring session review'
@ddt.data(
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected
)
def test_send_email(self, status):
"""
Assert that email is sent on the following statuses of proctoring attempt.
"""
exam_attempt = self._create_started_exam_attempt()
credit_state = get_runtime_service('credit').get_credit_state(self.user_id, self.course_id)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 1)
self.assertIn(self.proctored_exam_email_subject, mail.outbox[0].subject)
self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body)
self.assertIn(ProctoredExamStudentAttemptStatus.get_status_alias(status), mail.outbox[0].body)
self.assertIn(credit_state['course_name'], mail.outbox[0].body)
@ddt.data(
ProctoredExamStudentAttemptStatus.second_review_required,
ProctoredExamStudentAttemptStatus.error
)
def test_email_not_sent(self, status):
"""
Assert than email is not sent on the following statuses of proctoring attempt
"""
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
def test_send_email_unicode(self):
"""
Assert that email can be sent with a unicode course name.
"""
course_name = u'अआईउऊऋऌ अआईउऊऋऌ'
set_runtime_service('credit', MockCreditService(course_name=course_name))
exam_attempt = self._create_started_exam_attempt()
credit_state = get_runtime_service('credit').get_credit_state(self.user_id, self.course_id)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
ProctoredExamStudentAttemptStatus.submitted
)
self.assertEquals(len(mail.outbox), 1)
self.assertIn(self.proctored_exam_email_subject, mail.outbox[0].subject)
self.assertIn(course_name, mail.outbox[0].subject)
self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body)
self.assertIn(
ProctoredExamStudentAttemptStatus.get_status_alias(
ProctoredExamStudentAttemptStatus.submitted
),
mail.outbox[0].body
)
self.assertIn(credit_state['course_name'], mail.outbox[0].body)
@ddt.data(
ProctoredExamStudentAttemptStatus.eligible,
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
ProctoredExamStudentAttemptStatus.ready_to_start,
ProctoredExamStudentAttemptStatus.started,
ProctoredExamStudentAttemptStatus.ready_to_submit,
ProctoredExamStudentAttemptStatus.declined,
ProctoredExamStudentAttemptStatus.timed_out,
ProctoredExamStudentAttemptStatus.error
)
@patch.dict('settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_not_send_email(self, status):
"""
Assert that email is not sent on the following statuses of proctoring attempt.
"""
exam_attempt = self._create_started_exam_attempt()
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
@ddt.data(
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected
)
def test_not_send_email_sample_exam(self, status):
"""
Assert that email is not sent when there is practice/sample exam
"""
exam_attempt = self._create_started_exam_attempt(is_sample_attempt=True)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
@ddt.data(
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected
)
def test_not_send_email_timed_exam(self, status):
"""
Assert that email is not sent when exam is timed/not-proctoring
"""
exam_attempt = self._create_started_exam_attempt(is_proctored=False)
update_attempt_status(
exam_attempt.proctored_exam_id,
self.user.id,
status
)
self.assertEquals(len(mail.outbox), 0)
# coding=utf-8
# pylint: disable=too-many-lines, invalid-name
"""
All tests for the api.py
"""
import ddt
from datetime import datetime, timedelta
from mock import patch
import pytz
from freezegun import freeze_time
from edx_proctoring.api import (
update_exam,
get_exam_by_id,
add_allowance_for_user,
get_exam_attempt,
get_student_view,
update_attempt_status,
)
from edx_proctoring.models import (
ProctoredExam,
ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
)
from edx_proctoring.runtime import set_runtime_service
from .test_services import MockCreditServiceWithCourseEndDate
from .utils import ProctoredExamTestCase
@ddt.ddt
class ProctoredExamStudentViewTests(ProctoredExamTestCase):
"""
All tests for the student view
"""
def setUp(self):
"""
Build out test harnessing
"""
super(ProctoredExamStudentViewTests, self).setUp()
# Messages for get_student_view
self.start_an_exam_msg = 'This exam is proctored'
self.exam_expired_msg = 'The due date for this exam has passed'
self.timed_exam_msg = '{exam_name} is a Timed Exam'
self.timed_exam_submitted = 'You have submitted your timed exam.'
self.timed_exam_expired = 'The time allotted for this exam has expired.'
self.submitted_timed_exam_msg_with_due_date = 'After the due date has passed,'
self.exam_time_expired_msg = 'You did not complete the exam in the allotted time'
self.exam_time_error_msg = 'A technical error has occurred with your proctored exam'
self.chose_proctored_exam_msg = 'Follow these steps to set up and start your proctored exam'
self.proctored_exam_optout_msg = 'Take this exam as an open exam instead'
self.proctored_exam_completed_msg = 'Are you sure you want to end your proctored exam'
self.proctored_exam_waiting_for_app_shutdown_msg = 'You are about to complete your proctored exam'
self.proctored_exam_submitted_msg = 'You have submitted this proctored exam for review'
self.proctored_exam_verified_msg = 'Your proctoring session was reviewed and passed all requirements'
self.proctored_exam_rejected_msg = 'Your proctoring session was reviewed and did not pass requirements'
self.start_a_practice_exam_msg = 'Get familiar with proctoring for real exams later in the course'
self.practice_exam_submitted_msg = 'You have submitted this practice proctored exam'
self.practice_exam_created_msg = 'Follow these steps to set up and start your proctored exam'
self.practice_exam_completion_msg = 'Are you sure you want to end your proctored exam'
self.ready_to_start_msg = 'Follow these instructions'
self.practice_exam_failed_msg = 'There was a problem with your practice proctoring session'
self.footer_msg = 'About Proctored Exams'
self.timed_footer_msg = 'Can I request additional time to complete my exam?'
def test_get_student_view(self):
"""
Test for get_student_view prompting the user to take the exam
as a timed exam or a proctored exam.
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'hide_after_due': False,
}
)
self.assertIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
# try practice exam variant
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id + 'foo',
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'is_practice_exam': True,
'hide_after_due': False,
}
)
self.assertIn(self.start_a_practice_exam_msg.format(exam_name=self.exam_name), rendered_response)
def test_get_honor_view_with_practice_exam(self):
"""
Test for get_student_view prompting when the student is enrolled in non-verified
track for a practice exam, this should return not None, meaning
student will see proctored content
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'honor'
},
'is_practice_exam': True
}
)
self.assertIsNotNone(rendered_response)
def test_get_honor_view(self):
"""
Test for get_student_view prompting when the student is enrolled in non-verified
track, this should return None
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'honor'
},
'is_practice_exam': False
}
)
self.assertIsNone(rendered_response)
@ddt.data(
('reverification', None, 'The following prerequisites are in a <strong>pending</strong> state', True),
('reverification', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('reverification', 'failed', 'You did not satisfy the following prerequisites', True),
('reverification', 'satisfied', 'To be eligible to earn credit for this course', False),
('reverification', 'declined', None, False),
('proctored_exam', None, 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'failed', 'You did not satisfy the following prerequisites', True),
('proctored_exam', 'satisfied', 'To be eligible to earn credit for this course', False),
('proctored_exam', 'declined', None, False),
('grade', 'failed', 'To be eligible to earn credit for this course', False),
# this is nonesense, but let's double check it
('grade', 'declined', 'To be eligible to earn credit for this course', False),
)
@ddt.unpack
def test_prereq_scenarios(self, namespace, req_status, expected_content, should_see_prereq):
"""
This test asserts that proctoring will not be displayed under the following
conditions:
- Verified student has not completed all 'reverification' requirements
"""
exam = get_exam_by_id(self.proctored_exam_id)
# user hasn't attempted reverifications
rendered_response = get_student_view(
user_id=self.user_id,
course_id=exam['course_id'],
content_id=exam['content_id'],
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'is_practice_exam': False,
'credit_state': {
'enrollment_mode': 'verified',
'credit_requirement_status': [
{
'namespace': namespace,
'name': 'foo',
'display_name': 'Foo Requirement',
'status': req_status,
'order': 0
}
]
},
}
)
if expected_content:
self.assertIn(expected_content, rendered_response)
else:
self.assertIsNone(rendered_response)
if req_status == 'declined' and not expected_content:
# also we should have auto-declined if a pre-requisite was declined
attempt = get_exam_attempt(exam['id'], self.user_id)
self.assertIsNotNone(attempt)
self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.declined)
if should_see_prereq:
self.assertIn('Foo Requirement', rendered_response)
def test_student_view_non_student(self):
"""
Make sure that if we ask for a student view if we are not in a student role,
then we don't see any proctoring views
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
},
user_role='staff'
)
self.assertIsNone(rendered_response)
def test_wrong_exam_combo(self):
"""
Verify that we get a None back when rendering a view
for a practice, non-proctored exam. This is unsupported.
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id='foo',
content_id='bar',
context={
'is_proctored': False,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'hide_after_due': False,
},
user_role='student'
)
self.assertIsNone(rendered_response)
def test_proctored_exam_passed_end_date(self):
"""
Verify that we get a None back on a proctored exam
if the course end date is passed
"""
set_runtime_service('credit', MockCreditServiceWithCourseEndDate())
rendered_response = get_student_view(
user_id=self.user_id,
course_id='foo',
content_id='bar',
context={
'is_proctored': True,
'is_practice_exam': False,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'due_date': None,
'hide_after_due': False,
},
user_role='student'
)
self.assertIsNone(rendered_response)
def test_practice_exam_passed_end_date(self):
"""
Verify that we get a None back on a practice exam
if the course end date is passed
"""
set_runtime_service('credit', MockCreditServiceWithCourseEndDate())
rendered_response = get_student_view(
user_id=self.user_id,
course_id='foo',
content_id='bar',
context={
'is_proctored': True,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'due_date': None,
'hide_after_due': False,
},
user_role='student'
)
self.assertIsNone(rendered_response)
def test_get_disabled_student_view(self):
"""
Assert that a disabled proctored exam will not override the
student_view
"""
self.assertIsNone(
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.disabled_content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
)
def test_get_studentview_unstarted_exam(self):
"""
Test for get_student_view proctored exam which has not started yet.
"""
self._create_unstarted_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertIn(self.proctored_exam_optout_msg, rendered_response)
# now make sure content remains the same if
# the status transitions to 'download_software_clicked'
update_attempt_status(
self.proctored_exam_id,
self.user_id,
ProctoredExamStudentAttemptStatus.download_software_clicked
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertIn(self.proctored_exam_optout_msg, rendered_response)
def test_get_studentview_unstarted_practice_exam(self):
"""
Test for get_student_view Practice exam which has not started yet.
"""
self._create_unstarted_exam_attempt(is_practice=True)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'is_practice_exam': True,
'default_time_limit_mins': 90
}
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertNotIn(self.proctored_exam_optout_msg, rendered_response)
def test_declined_attempt(self):
"""
Make sure that a declined attempt does not show proctoring
"""
attempt_obj = self._create_unstarted_exam_attempt()
attempt_obj.status = ProctoredExamStudentAttemptStatus.declined
attempt_obj.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
def test_get_studentview_ready(self):
"""
Assert that we get the right content
when the exam is ready to be started
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_start
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.ready_to_start_msg, rendered_response)
def test_get_studentview_started_exam(self):
"""
Test for get_student_view proctored exam which has started.
"""
self._create_started_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
def test_get_studentview_started_practice_exam(self):
"""
Test for get_student_view practice proctored exam which has started.
"""
self._create_started_practice_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
def test_get_studentview_started_timed_exam(self):
"""
Test for get_student_view timed exam which has started.
"""
self._create_started_exam_attempt(is_proctored=False)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
@ddt.data(True, False)
def test_get_studentview_long_limit(self, under_exception):
"""
Test for hide_extra_time_footer on exams with > 20 hours time limit
"""
exam_id = self._create_exam_with_due_time(is_proctored=False, )
if under_exception:
update_exam(exam_id, time_limit_mins=((20 * 60))) # exactly 20 hours
else:
update_exam(exam_id, time_limit_mins=((20 * 60) + 1)) # 1 minute greater than 20 hours
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': False,
'display_name': self.exam_name,
}
)
if under_exception:
self.assertIn(self.timed_footer_msg, rendered_response)
else:
self.assertNotIn(self.timed_footer_msg, rendered_response)
@ddt.data(
(datetime.now(pytz.UTC) + timedelta(days=1), False),
(datetime.now(pytz.UTC) - timedelta(days=1), False),
(datetime.now(pytz.UTC) - timedelta(days=1), True),
)
@ddt.unpack
def test_get_studentview_submitted_timed_exam_with_past_due_date(self, due_date, hide_after_due):
"""
Test for get_student_view timed exam with the due date.
"""
# exam is created with due datetime which has already passed
exam_id = self._create_exam_with_due_time(is_proctored=False, due_date=due_date)
if hide_after_due:
update_exam(exam_id, hide_after_due=hide_after_due)
# now create the timed_exam attempt in the submitted state
self._create_exam_attempt(exam_id, status='submitted')
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 10,
'due_date': due_date,
}
)
if datetime.now(pytz.UTC) < due_date:
self.assertIn(self.timed_exam_submitted, rendered_response)
self.assertIn(self.submitted_timed_exam_msg_with_due_date, rendered_response)
elif hide_after_due:
self.assertIn(self.timed_exam_submitted, rendered_response)
self.assertNotIn(self.submitted_timed_exam_msg_with_due_date, rendered_response)
else:
self.assertIsNone(rendered_response)
def test_proctored_exam_attempt_with_past_due_datetime(self):
"""
Test for get_student_view for proctored exam with past due datetime
"""
due_date = datetime.now(pytz.UTC) + timedelta(days=1)
# exam is created with due datetime which has already passed
self._create_exam_with_due_time(due_date=due_date)
# due_date is exactly after 24 hours, if student arrives after 2 days
# then he can not attempt the proctored exam
reset_time = due_date + timedelta(days=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
# call the view again, because the first call set the exam attempt to 'expired'
# this second call will render the view based on the state
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': True,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
def test_timed_exam_attempt_with_past_due_datetime(self):
"""
Test for get_student_view for timed exam with past due datetime
"""
due_date = datetime.now(pytz.UTC) + timedelta(days=1)
# exam is created with due datetime which has already passed
self._create_exam_with_due_time(
due_date=due_date,
is_proctored=False
)
# due_date is exactly after 24 hours, if student arrives after 2 days
# then he can not attempt the proctored exam
reset_time = due_date + timedelta(days=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
# call the view again, because the first call set the exam attempt to 'expired'
# this second call will render the view based on the state
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': True,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': self.default_time_limit,
'due_date': due_date,
}
)
self.assertIn(self.exam_expired_msg, rendered_response)
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_get_studentview_timedout(self):
"""
Verifies that if we call get_studentview when the timer has expired
it will automatically state transition into timed_out
"""
self._create_started_exam_attempt()
reset_time = datetime.now(pytz.UTC) + timedelta(days=1)
with freeze_time(reset_time):
with self.assertRaises(NotImplementedError):
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
def test_get_studentview_submitted_status(self):
"""
Test for get_student_view proctored exam which has been submitted.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.submitted
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_waiting_for_app_shutdown_msg, rendered_response)
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
# now make sure if this status transitions to 'second_review_required'
# the student will still see a 'submitted' message
update_attempt_status(
exam_attempt.proctored_exam_id,
exam_attempt.user_id,
ProctoredExamStudentAttemptStatus.second_review_required
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
def test_get_studentview_submitted_status_with_duedate(self):
"""
Test for get_student_view proctored exam which has been submitted
And due date has passed
"""
proctored_exam = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=30,
is_proctored=True,
is_active=True,
due_date=datetime.now(pytz.UTC) + timedelta(minutes=40)
)
exam_attempt = ProctoredExamStudentAttempt.objects.create(
proctored_exam=proctored_exam,
user=self.user,
allowed_time_limit_mins=30,
taking_as_proctored=True,
external_id=proctored_exam.external_id,
status=ProctoredExamStudentAttemptStatus.submitted,
last_poll_timestamp=datetime.now(pytz.UTC)
)
# due date is after 10 minutes
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=20)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user.id,
course_id='a/b/c',
content_id='test_content',
context={
'is_proctored': True,
'display_name': 'Test Exam',
'default_time_limit_mins': 30
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
exam_attempt.is_status_acknowledged = True
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user.id,
course_id='a/b/c',
content_id='test_content',
context={
'is_proctored': True,
'display_name': 'Test Exam',
'default_time_limit_mins': 30
}
)
self.assertIsNotNone(rendered_response)
def test_get_studentview_submitted_status_practiceexam(self):
"""
Test for get_student_view practice exam which has been submitted.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.submitted
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_waiting_for_app_shutdown_msg, rendered_response)
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_submitted_msg, rendered_response)
@ddt.data(
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
)
def test_get_studentview_created_status_practiceexam(self, status):
"""
Test for get_student_view practice exam which has been created.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = status
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_created_msg, rendered_response)
def test_get_studentview_ready_to_start_status_practiceexam(self):
"""
Test for get_student_view practice exam which is ready to start.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_start
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.ready_to_start_msg, rendered_response)
def test_get_studentview_complete_status_practiceexam(self):
"""
Test for get_student_view practice exam when it is complete/ready to submit.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_submit
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_completion_msg, rendered_response)
def test_get_studentview_rejected_status(self):
"""
Test for get_student_view proctored exam which has been rejected.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.rejected
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_rejected_msg, rendered_response)
def test_get_studentview_verified_status(self):
"""
Test for get_student_view proctored exam which has been verified.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.verified
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_verified_msg, rendered_response)
def test_get_studentview_completed_status(self):
"""
Test for get_student_view proctored exam which has been completed.
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_submit
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_completed_msg, rendered_response)
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_get_studentview_expired(self):
"""
Test for get_student_view proctored exam which has expired. Since we don't have a template
for that view rendering, it will throw a NotImplementedError
"""
self._create_started_exam_attempt(started_at=datetime.now(pytz.UTC).replace(year=2010))
with self.assertRaises(NotImplementedError):
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
def test_get_studentview_erroneous_exam(self):
"""
Test for get_student_view proctored exam which has exam status error.
"""
ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id,
user_id=self.user_id,
external_id=self.external_id,
started_at=datetime.now(pytz.UTC),
allowed_time_limit_mins=10,
status='error'
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 10
}
)
self.assertIn(self.exam_time_error_msg, rendered_response)
def test_get_studentview_erroneous_practice_exam(self):
"""
Test for get_student_view practice exam which has exam status error.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.error
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_failed_msg, rendered_response)
def test_get_studentview_unstarted_timed_exam(self):
"""
Test for get_student_view Timed exam which is not proctored and has not started yet.
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id="abc",
content_id=self.content_id,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'hide_after_due': False,
}
)
self.assertNotIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response)
self.assertIn('1 hour and 30 minutes', rendered_response)
self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
def test_get_studentview_unstarted_timed_exam_with_allowance(self):
"""
Test for get_student_view Timed exam which is not proctored and has not started yet.
But user has an allowance
"""
allowed_extra_time = 10
add_allowance_for_user(
self.timed_exam_id,
self.user.username,
ProctoredExamStudentAllowance.ADDITIONAL_TIME_GRANTED,
str(allowed_extra_time)
)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={}
)
self.assertNotIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response)
self.assertIn('31 minutes', rendered_response)
self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
@ddt.data(
(
ProctoredExamStudentAttemptStatus.ready_to_submit,
'Are you sure that you want to submit your timed exam?'
),
(
ProctoredExamStudentAttemptStatus.submitted,
'You have submitted your timed exam'
),
)
@ddt.unpack
def test_get_studentview_completed_timed_exam(self, status, expected_content):
"""
Test for get_student_view timed exam which has completed.
"""
exam_attempt = self._create_started_exam_attempt(is_proctored=False)
exam_attempt.status = status
if status == 'submitted':
exam_attempt.completed_at = datetime.now(pytz.UTC)
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'display_name': self.exam_name,
}
)
self.assertIn(expected_content, rendered_response)
def test_expired_exam(self):
"""
Test that an expired exam shows a difference message when the exam is expired just recently
"""
# create exam with completed_at equal to current time and started_at equal to allowed_time_limit_mins ago
attempt = self._create_started_exam_attempt(is_proctored=False)
attempt.status = "submitted"
attempt.started_at = attempt.started_at - timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.completed_at = attempt.started_at + timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'display_name': self.exam_name,
}
)
self.assertIn(self.timed_exam_expired, rendered_response)
# update start and completed time such that completed_time is allowed_time_limit_mins ago than the current time
attempt.started_at = attempt.started_at - timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.completed_at = attempt.completed_at - timedelta(minutes=attempt.allowed_time_limit_mins)
attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'display_name': self.exam_name,
}
)
self.assertIn(self.timed_exam_submitted, rendered_response)
@ddt.data(
ProctoredExamStudentAttemptStatus.eligible,
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.verified,
ProctoredExamStudentAttemptStatus.rejected,
)
def test_footer_present(self, status):
"""
Make sure the footer content is visible in the rendered output
"""
if status != ProctoredExamStudentAttemptStatus.eligible:
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = status
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNotNone(rendered_response)
if status == ProctoredExamStudentAttemptStatus.submitted:
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.footer_msg, rendered_response)
else:
self.assertIn(self.footer_msg, rendered_response)
# coding=utf-8
# pylint: disable=invalid-name
"""
Subclasses Django test client to allow for easy login
"""
from datetime import datetime
from importlib import import_module
import pytz
from django.conf import settings
from django.contrib.auth import login
......@@ -11,6 +16,22 @@ from django.test.client import Client
from django.test import TestCase
from django.contrib.auth.models import User
from edx_proctoring.api import (
create_exam,
)
from edx_proctoring.models import (
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
)
from edx_proctoring.tests.test_services import (
MockCreditService,
MockInstructorService,
)
from edx_proctoring.runtime import set_runtime_service
from eventtracking import tracker
from eventtracking.tracker import Tracker, TRACKERS
class TestClient(Client):
"""
......@@ -64,3 +85,256 @@ class LoggedInTestCase(TestCase):
self.user = User(username='tester', email='tester@test.com')
self.user.save()
self.client.login_user(self.user)
class MockTracker(Tracker):
"""
A mocked out tracker which implements the emit method
"""
def emit(self, name=None, data=None):
"""
Overload this method to do nothing
"""
pass
class ProctoredExamTestCase(LoggedInTestCase):
"""
All tests for the models.py
"""
def setUp(self):
"""
Build out test harnessing
"""
super(ProctoredExamTestCase, self).setUp()
self.default_time_limit = 21
self.course_id = 'test_course'
self.content_id_for_exam_with_due_date = 'test_content_due_date_id'
self.content_id = 'test_content_id'
self.content_id_timed = 'test_content_id_timed'
self.content_id_practice = 'test_content_id_practice'
self.disabled_content_id = 'test_disabled_content_id'
self.exam_name = 'Test Exam'
self.user_id = self.user.id
self.key = 'additional_time_granted'
self.value = '10'
self.external_id = 'test_external_id'
self.proctored_exam_id = self._create_proctored_exam()
self.timed_exam_id = self._create_timed_exam()
self.practice_exam_id = self._create_practice_exam()
self.disabled_exam_id = self._create_disabled_exam()
set_runtime_service('credit', MockCreditService())
set_runtime_service('instructor', MockInstructorService(is_user_course_staff=True))
tracker.register_tracker(MockTracker())
self.prerequisites = [
{
'namespace': 'proctoring',
'name': 'proc1',
'order': 2,
'status': 'satisfied',
},
{
'namespace': 'reverification',
'name': 'rever1',
'order': 1,
'status': 'satisfied',
},
{
'namespace': 'grade',
'name': 'grade1',
'order': 0,
'status': 'pending',
},
{
'namespace': 'reverification',
'name': 'rever2',
'order': 3,
'status': 'failed',
},
{
'namespace': 'proctoring',
'name': 'proc2',
'order': 4,
'status': 'pending',
},
]
self.declined_prerequisites = [
{
'namespace': 'proctoring',
'name': 'proc1',
'order': 2,
'status': 'satisfied',
},
{
'namespace': 'reverification',
'name': 'rever1',
'order': 1,
'status': 'satisfied',
},
{
'namespace': 'grade',
'name': 'grade1',
'order': 0,
'status': 'pending',
},
{
'namespace': 'reverification',
'name': 'rever2',
'order': 3,
'status': 'declined',
},
{
'namespace': 'proctoring',
'name': 'proc2',
'order': 4,
'status': 'pending',
},
]
def tearDown(self):
"""
Cleanup
"""
del TRACKERS['default']
def _create_proctored_exam(self):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit
)
def _create_exam_with_due_time(self, is_proctored=True, is_practice_exam=False, due_date=None):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_proctored=is_proctored,
is_practice_exam=is_practice_exam,
due_date=due_date
)
def _create_timed_exam(self):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_timed,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_proctored=False
)
def _create_practice_exam(self):
"""
Calls the api's create_exam to create a practice exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_practice,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_practice_exam=True,
is_proctored=True
)
def _create_disabled_exam(self):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
is_proctored=False,
content_id=self.disabled_content_id,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_active=False
)
def _create_exam_attempt(self, exam_id, status='created'):
"""
Creates the ProctoredExamStudentAttempt object.
"""
attempt = ProctoredExamStudentAttempt(
proctored_exam_id=exam_id,
user_id=self.user_id,
external_id=self.external_id,
allowed_time_limit_mins=10,
status=status
)
if status in (ProctoredExamStudentAttemptStatus.started,
ProctoredExamStudentAttemptStatus.ready_to_submit, ProctoredExamStudentAttemptStatus.submitted):
attempt.started_at = datetime.now(pytz.UTC)
if ProctoredExamStudentAttemptStatus.is_completed_status(status):
attempt.completed_at = datetime.now(pytz.UTC)
attempt.save()
return attempt
def _create_unstarted_exam_attempt(self, is_proctored=True, is_practice=False):
"""
Creates the ProctoredExamStudentAttempt object.
"""
if is_proctored:
if is_practice:
exam_id = self.practice_exam_id
else:
exam_id = self.proctored_exam_id
else:
exam_id = self.timed_exam_id
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=exam_id,
user_id=self.user_id,
external_id=self.external_id,
allowed_time_limit_mins=10,
status='created'
)
def _create_started_exam_attempt(self, started_at=None, is_proctored=True, is_sample_attempt=False):
"""
Creates the ProctoredExamStudentAttempt object.
"""
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id if is_proctored else self.timed_exam_id,
user_id=self.user_id,
external_id=self.external_id,
started_at=started_at if started_at else datetime.now(pytz.UTC),
status=ProctoredExamStudentAttemptStatus.started,
allowed_time_limit_mins=10,
taking_as_proctored=is_proctored,
is_sample_attempt=is_sample_attempt
)
def _create_started_practice_exam_attempt(self, started_at=None):
"""
Creates the ProctoredExamStudentAttempt object.
"""
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.practice_exam_id,
taking_as_proctored=True,
user_id=self.user_id,
external_id=self.external_id,
started_at=started_at if started_at else datetime.now(pytz.UTC),
is_sample_attempt=True,
status=ProctoredExamStudentAttemptStatus.started,
allowed_time_limit_mins=10
)
......@@ -5,6 +5,8 @@ djangorestframework>=3.1,<3.2
django-ipware==1.1.0
pytz>=2012h
pycrypto>=2.6
python-dateutil==2.1
# edX packages
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/opaque-keys.git@27dc382ea587483b1e3889a3d19cbd90b9023a06#egg=opaque-keys
edx-opaque-keys==0.3.4
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment