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, ...@@ -525,9 +525,12 @@ def get_student_view(user_id, course_id, content_id,
has_started_exam = attempt and attempt.get('started_at') has_started_exam = attempt and attempt.get('started_at')
has_time_expired = False has_time_expired = False
if has_started_exam: if has_started_exam:
now_utc = datetime.now(pytz.UTC) if attempt.get('status') == 'error':
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins']) student_view_template = 'proctoring/seq_proctored_exam_error.html'
has_time_expired = now_utc > expires_at 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 # make sure the attempt has been marked as timed_out, if need be
if has_time_expired and attempt['status'] != ProctoredExamStudentAttemptStatus.timed_out: if has_time_expired and attempt['status'] != ProctoredExamStudentAttemptStatus.timed_out:
......
...@@ -78,7 +78,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer): ...@@ -78,7 +78,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
fields = ( fields = (
"id", "created", "modified", "user", "started_at", "completed_at", "id", "created", "modified", "user", "started_at", "completed_at",
"external_id", "status", "proctored_exam", "allowed_time_limit_mins", "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 @@ ...@@ -5,6 +5,8 @@
defaults: { defaults: {
in_timed_exam: false, in_timed_exam: false,
attempt_id: 0,
attempt_status: 'started',
taking_as_proctored: false, taking_as_proctored: false,
exam_display_name: '', exam_display_name: '',
exam_url_path: '', exam_url_path: '',
......
...@@ -13,6 +13,7 @@ var edx = edx || {}; ...@@ -13,6 +13,7 @@ var edx = edx || {};
this.templateId = options.proctored_template; this.templateId = options.proctored_template;
this.template = null; this.template = null;
this.timerId = null; this.timerId = null;
this.timerTick = 0;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */ /* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this.grace_period_secs = 5; this.grace_period_secs = 5;
...@@ -53,7 +54,11 @@ var edx = edx || {}; ...@@ -53,7 +54,11 @@ var edx = edx || {};
}, },
render: function () { render: function () {
if (this.template !== null) { 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()); var html = this.template(this.model.toJSON());
this.$el.html(html); this.$el.html(html);
this.$el.show(); this.$el.show();
...@@ -71,6 +76,22 @@ var edx = edx || {}; ...@@ -71,6 +76,22 @@ var edx = edx || {};
"credit eligibility in this course.\n"); "credit eligibility in this course.\n");
}, },
updateRemainingTime: function (self) { 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').removeClass("low-time warning critical");
self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState()); self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState());
self.$el.find('span#time_remaining_id b').html(self.model.getFormattedRemainingTime()); 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): ...@@ -75,6 +75,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.start_an_exam_msg = 'Would you like to take %s as a proctored exam?' 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.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_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' self.chose_proctored_exam_msg = 'You have chosen to take %s as a proctored exam'
def _create_proctored_exam(self): def _create_proctored_exam(self):
...@@ -606,4 +607,31 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -606,4 +607,31 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90 'default_time_limit_mins': 90
} }
) )
self.assertIn(self.exam_time_expired_msg, rendered_response) 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 ...@@ -5,9 +5,10 @@ All tests for the proctored_exams.py
import json import json
import pytz import pytz
from mock import Mock from mock import Mock
from freezegun import freeze_time
from httmock import HTTMock from httmock import HTTMock
from string import Template # pylint: disable=deprecated-module 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.test.client import Client
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -488,6 +489,61 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -488,6 +489,61 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertIsNotNone(response_data['started_at']) self.assertIsNotNone(response_data['started_at'])
self.assertIsNone(response_data['completed_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): def test_remove_attempt(self):
""" """
Confirms that an attempt can be removed Confirms that an attempt can be removed
......
...@@ -29,7 +29,9 @@ from edx_proctoring.api import ( ...@@ -29,7 +29,9 @@ from edx_proctoring.api import (
get_exam_attempt_by_id, get_exam_attempt_by_id,
get_all_exam_attempts, get_all_exam_attempts,
remove_exam_attempt, remove_exam_attempt,
get_filtered_exam_attempts) get_filtered_exam_attempts,
update_exam_attempt
)
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredBaseException, ProctoredBaseException,
ProctoredExamNotFoundException, ProctoredExamNotFoundException,
...@@ -43,6 +45,8 @@ from .utils import AuthenticatedAPIView ...@@ -43,6 +45,8 @@ from .utils import AuthenticatedAPIView
ATTEMPTS_PER_PAGE = 25 ATTEMPTS_PER_PAGE = 25
SOFTWARE_SECURE_CLIENT_TIMEOUT = 15
LOG = logging.getLogger("edx_proctoring_views") LOG = logging.getLogger("edx_proctoring_views")
...@@ -265,6 +269,15 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -265,6 +269,15 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
) )
raise ProctoredExamPermissionDenied(err_msg) 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( return Response(
data=attempt, data=attempt,
status=status.HTTP_200_OK status=status.HTTP_200_OK
...@@ -482,6 +495,8 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView): ...@@ -482,6 +495,8 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
'low_threshold_sec': low_threshold, 'low_threshold_sec': low_threshold,
'critically_low_threshold_sec': critically_low_threshold, 'critically_low_threshold_sec': critically_low_threshold,
'course_id': exam['course_id'], 'course_id': exam['course_id'],
'attempt_id': attempt['id'],
'attempt_status': attempt['status']
} }
else: else:
response_dict = { 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