Commit 7174a034 by ibrahimahmed443 Committed by Chris Dodge

add expiry message when exam times out

parent 74eaaa9f
...@@ -50,6 +50,8 @@ from edx_proctoring.runtime import get_runtime_service ...@@ -50,6 +50,8 @@ from edx_proctoring.runtime import get_runtime_service
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
SHOW_EXPIRY_MESSAGE_DURATION = 1 * 60 # duration within which expiry message is shown for a timed-out exam
def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None, def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None,
is_proctored=True, is_practice_exam=False, external_id=None, is_active=True): is_proctored=True, is_practice_exam=False, external_id=None, is_active=True):
...@@ -1272,6 +1274,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id): ...@@ -1272,6 +1274,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
""" """
student_view_template = None student_view_template = None
attempt = get_exam_attempt(exam_id, user_id) attempt = get_exam_attempt(exam_id, user_id)
has_time_expired = False
attempt_status = attempt['status'] if attempt else None attempt_status = attempt['status'] if attempt else None
has_due_date = True if exam['due_date'] is not None else False has_due_date = True if exam['due_date'] is not None else False
...@@ -1293,6 +1296,18 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id): ...@@ -1293,6 +1296,18 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
student_view_template = 'timed_exam/submitted.html' student_view_template = 'timed_exam/submitted.html'
current_datetime = datetime.now(pytz.UTC)
start_time = attempt['started_at']
end_time = attempt['completed_at']
attempt_duration_sec = (end_time - start_time).total_seconds()
allowed_duration_sec = attempt['allowed_time_limit_mins'] * 60
since_exam_ended_sec = (current_datetime - end_time).total_seconds()
# if the user took >= the available time, then the exam must have expired.
# but we show expiry message only when the exam was taken recently (less than SHOW_EXPIRY_MESSAGE_DURATION)
if attempt_duration_sec >= allowed_duration_sec and since_exam_ended_sec < SHOW_EXPIRY_MESSAGE_DURATION:
has_time_expired = True
if student_view_template: if student_view_template:
template = loader.get_template(student_view_template) template = loader.get_template(student_view_template)
django_context = Context(context) django_context = Context(context)
...@@ -1335,6 +1350,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id): ...@@ -1335,6 +1350,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
'exam_name': exam['exam_name'], 'exam_name': exam['exam_name'],
'progress_page_url': progress_page_url, 'progress_page_url': progress_page_url,
'does_time_remain': _does_time_remain(attempt), 'does_time_remain': _does_time_remain(attempt),
'has_time_expired': has_time_expired,
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'), 'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
'change_state_url': reverse( 'change_state_url': reverse(
'edx_proctoring.proctored_exam.attempt', 'edx_proctoring.proctored_exam.attempt',
......
{% load i18n %} {% load i18n %}
<div class="sequence proctored-exam completed" data-exam-id="{{exam_id}}"> <div class="sequence proctored-exam completed" data-exam-id="{{exam_id}}">
<h3> <h3>
{% blocktrans %}
You have submitted your timed exam. {% if has_time_expired %}
{% endblocktrans %} {% blocktrans %}
The time allotted for this exam has expired. Your exam has been submitted and any work you completed will be graded.
{% endblocktrans %}
{% else %}
{% blocktrans %}
You have submitted your timed exam.
{% endblocktrans %}
{% endif %}
</h3> </h3>
<hr> <hr>
<p> <p>
......
...@@ -103,6 +103,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -103,6 +103,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.start_an_exam_msg = 'This exam is proctored' self.start_an_exam_msg = 'This exam is proctored'
self.exam_expired_msg = 'The due date for this exam has passed' 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_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.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_expired_msg = 'You did not complete the exam in the allotted time'
self.exam_time_error_msg = 'There was a problem with your proctoring session' self.exam_time_error_msg = 'There was a problem with your proctoring session'
...@@ -259,7 +261,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -259,7 +261,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
Creates the ProctoredExamStudentAttempt object. Creates the ProctoredExamStudentAttempt object.
""" """
return ProctoredExamStudentAttempt.objects.create(
attempt = ProctoredExamStudentAttempt(
proctored_exam_id=exam_id, proctored_exam_id=exam_id,
user_id=self.user_id, user_id=self.user_id,
external_id=self.external_id, external_id=self.external_id,
...@@ -267,6 +270,17 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -267,6 +270,17 @@ class ProctoredExamApiTests(LoggedInTestCase):
status=status 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): def _create_unstarted_exam_attempt(self, is_proctored=True, is_practice=False):
""" """
Creates the ProctoredExamStudentAttempt object. Creates the ProctoredExamStudentAttempt object.
...@@ -1676,6 +1690,9 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1676,6 +1690,9 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
exam_attempt = self._create_started_exam_attempt(is_proctored=False) exam_attempt = self._create_started_exam_attempt(is_proctored=False)
exam_attempt.status = status exam_attempt.status = status
if status == 'submitted':
exam_attempt.completed_at = datetime.now(pytz.UTC)
exam_attempt.save() exam_attempt.save()
rendered_response = get_student_view( rendered_response = get_student_view(
...@@ -1688,6 +1705,44 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1688,6 +1705,44 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(expected_content, rendered_response) 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): def test_submitted_credit_state(self):
""" """
Verify that putting an attempt into the submitted state will also mark Verify that putting an attempt into the submitted state will also mark
......
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