Commit 487f461a by Muhammad Shoaib Committed by Chris Dodge

SOL-1087

parent 71caf92c
......@@ -525,9 +525,12 @@ def get_student_view(user_id, course_id, content_id,
has_started_exam = attempt and attempt.get('started_at')
has_time_expired = False
if has_started_exam:
now_utc = datetime.now(pytz.UTC)
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
has_time_expired = now_utc > expires_at
if attempt.get('status') == 'error':
student_view_template = 'proctoring/seq_proctored_exam_error.html'
else:
now_utc = datetime.now(pytz.UTC)
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
has_time_expired = now_utc > expires_at
# make sure the attempt has been marked as timed_out, if need be
if has_time_expired and attempt['status'] != ProctoredExamStudentAttemptStatus.timed_out:
......
......@@ -78,7 +78,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
fields = (
"id", "created", "modified", "user", "started_at", "completed_at",
"external_id", "status", "proctored_exam", "allowed_time_limit_mins",
"attempt_code", "is_sample_attempt", "taking_as_proctored"
"attempt_code", "is_sample_attempt", "taking_as_proctored", "last_poll_timestamp",
"last_poll_ipaddr"
)
......
......@@ -5,6 +5,8 @@
defaults: {
in_timed_exam: false,
attempt_id: 0,
attempt_status: 'started',
taking_as_proctored: false,
exam_display_name: '',
exam_url_path: '',
......
......@@ -13,6 +13,7 @@ var edx = edx || {};
this.templateId = options.proctored_template;
this.template = null;
this.timerId = null;
this.timerTick = 0;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this.grace_period_secs = 5;
......@@ -53,7 +54,11 @@ var edx = edx || {};
},
render: function () {
if (this.template !== null) {
if (this.model.get('in_timed_exam') && this.model.get('time_remaining_seconds') > 0) {
if (
this.model.get('in_timed_exam') &&
this.model.get('time_remaining_seconds') > 0 &&
this.model.get('attempt_status') !== 'error'
) {
var html = this.template(this.model.toJSON());
this.$el.html(html);
this.$el.show();
......@@ -71,6 +76,22 @@ var edx = edx || {};
"credit eligibility in this course.\n");
},
updateRemainingTime: function (self) {
self.timerTick ++;
if (self.timerTick % 5 == 0){
var url = self.model.url + '/' + self.model.get('attempt_id');
$.ajax(url).success(function(data) {
if (data.status === 'error') {
// Let the student know that his exam has failed due to an error.
// This alert may or may not bring the browser window back to the
// foreground (depending on browser as well as user settings)
alert(gettext('Your exam has failed'));
clearInterval(self.timerId); // stop the timer once the time finishes.
$(window).unbind('beforeunload', self.unloadMessage);
// refresh the page when the timer expired
location.reload();
}
});
}
self.$el.find('div.exam-timer').removeClass("low-time warning critical");
self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState());
self.$el.find('span#time_remaining_id b').html(self.model.getFormattedRemainingTime());
......
{% load i18n %}
<div class="sequence proctored-exam error">
<div class="gated-sequence">
{% trans "Your exam has been marked as failed due to an error." %}
</div>
</div>
......@@ -75,6 +75,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.start_an_exam_msg = 'Would you like to take %s as a proctored exam?'
self.timed_exam_msg = '%s is a Timed Exam'
self.exam_time_expired_msg = 'You did not complete the exam in the allotted time'
self.exam_time_error_msg = 'Your exam has been marked as failed due to an error.'
self.chose_proctored_exam_msg = 'You have chosen to take %s as a proctored exam'
def _create_proctored_exam(self):
......@@ -606,4 +607,31 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90
}
)
self.assertIn(self.exam_time_expired_msg, rendered_response)
def test_get_studentview_erroneous_exam(self): # pylint: disable=invalid-name
"""
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)
......@@ -5,9 +5,10 @@ All tests for the proctored_exams.py
import json
import pytz
from mock import Mock
from freezegun import freeze_time
from httmock import HTTMock
from string import Template # pylint: disable=deprecated-module
from datetime import datetime
from datetime import datetime, timedelta
from django.test.client import Client
from django.core.urlresolvers import reverse, NoReverseMatch
from django.contrib.auth.models import User
......@@ -488,6 +489,61 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertIsNotNone(response_data['started_at'])
self.assertIsNone(response_data['completed_at'])
def test_attempt_status_error(self):
"""
Test to confirm that attempt status is marked as error, because client
has stopped sending it's polling timestamp
"""
# Create an exam.
proctored_exam = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90
)
attempt_data = {
'exam_id': proctored_exam.id,
'external_id': proctored_exam.external_id,
'start_clock': True,
}
response = self.client.post(
reverse('edx_proctoring.proctored_exam.attempt.collection'),
attempt_data
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
attempt_id = response_data['exam_attempt_id']
self.assertEqual(attempt_id, 1)
response = self.client.get(
reverse('edx_proctoring.proctored_exam.attempt', args=[attempt_id])
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response_data['status'], 'started')
attempt_code = response_data['attempt_code']
# test the polling callback point
response = self.client.get(
reverse(
'edx_proctoring.anonymous.proctoring_poll_status',
args=[attempt_code]
)
)
self.assertEqual(response.status_code, 200)
# now reset the time to 2 minutes in the future.
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with freeze_time(reset_time):
response = self.client.get(
reverse('edx_proctoring.proctored_exam.attempt', args=[attempt_id])
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response_data['status'], 'error')
def test_remove_attempt(self):
"""
Confirms that an attempt can be removed
......
......@@ -29,7 +29,9 @@ from edx_proctoring.api import (
get_exam_attempt_by_id,
get_all_exam_attempts,
remove_exam_attempt,
get_filtered_exam_attempts)
get_filtered_exam_attempts,
update_exam_attempt
)
from edx_proctoring.exceptions import (
ProctoredBaseException,
ProctoredExamNotFoundException,
......@@ -43,6 +45,8 @@ from .utils import AuthenticatedAPIView
ATTEMPTS_PER_PAGE = 25
SOFTWARE_SECURE_CLIENT_TIMEOUT = 15
LOG = logging.getLogger("edx_proctoring_views")
......@@ -265,6 +269,15 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
)
raise ProctoredExamPermissionDenied(err_msg)
# check if the last_poll_timestamp is not None
# and if it is older than SOFTWARE_SECURE_CLIENT_TIMEOUT
# then attempt status should be marked as error.
last_poll_timestamp = attempt['last_poll_timestamp']
if last_poll_timestamp is not None \
and (datetime.now(pytz.UTC) - last_poll_timestamp).total_seconds() > SOFTWARE_SECURE_CLIENT_TIMEOUT:
attempt['status'] = 'error'
update_exam_attempt(attempt_id, status='error')
return Response(
data=attempt,
status=status.HTTP_200_OK
......@@ -482,6 +495,8 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
'low_threshold_sec': low_threshold,
'critically_low_threshold_sec': critically_low_threshold,
'course_id': exam['course_id'],
'attempt_id': attempt['id'],
'attempt_status': attempt['status']
}
else:
response_dict = {
......
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