Commit 40f36c45 by Chris Dodge

timer shouldn't start until the user acknowledges an alert in our window

parent 57320a34
...@@ -183,6 +183,17 @@ def get_exam_attempt_by_id(attempt_id): ...@@ -183,6 +183,17 @@ def get_exam_attempt_by_id(attempt_id):
return serialized_attempt_obj.data if exam_attempt_obj else None return serialized_attempt_obj.data if exam_attempt_obj else None
def get_exam_attempt_by_code(attempt_code):
"""
Signals the beginning of an exam attempt when we only have
an attempt code
"""
exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_code(attempt_code)
serialized_attempt_obj = ProctoredExamStudentAttemptSerializer(exam_attempt_obj)
return serialized_attempt_obj.data if exam_attempt_obj else None
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
""" """
Creates an exam attempt for user_id against exam_id. There should only be Creates an exam attempt for user_id against exam_id. There should only be
...@@ -264,7 +275,7 @@ def start_exam_attempt(exam_id, user_id): ...@@ -264,7 +275,7 @@ def start_exam_attempt(exam_id, user_id):
raise StudentExamAttemptDoesNotExistsException(err_msg) raise StudentExamAttemptDoesNotExistsException(err_msg)
_start_exam_attempt(existing_attempt) return _start_exam_attempt(existing_attempt)
def start_exam_attempt_by_code(attempt_code): def start_exam_attempt_by_code(attempt_code):
...@@ -283,7 +294,7 @@ def start_exam_attempt_by_code(attempt_code): ...@@ -283,7 +294,7 @@ def start_exam_attempt_by_code(attempt_code):
raise StudentExamAttemptDoesNotExistsException(err_msg) raise StudentExamAttemptDoesNotExistsException(err_msg)
_start_exam_attempt(existing_attempt) return _start_exam_attempt(existing_attempt)
def _start_exam_attempt(existing_attempt): def _start_exam_attempt(existing_attempt):
...@@ -302,6 +313,8 @@ def _start_exam_attempt(existing_attempt): ...@@ -302,6 +313,8 @@ def _start_exam_attempt(existing_attempt):
existing_attempt.start_exam_attempt() existing_attempt.start_exam_attempt()
return existing_attempt.id
def stop_exam_attempt(exam_id, user_id): def stop_exam_attempt(exam_id, user_id):
""" """
...@@ -330,6 +343,20 @@ def mark_exam_attempt_timeout(exam_id, user_id): ...@@ -330,6 +343,20 @@ def mark_exam_attempt_timeout(exam_id, user_id):
return exam_attempt_obj.id return exam_attempt_obj.id
def mark_exam_attempt_as_ready(exam_id, user_id):
"""
Marks the exam attemp as ready to start
"""
exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None:
raise StudentExamAttemptDoesNotExistsException('Error. Trying to time out an exam that does not exist.')
else:
exam_attempt_obj.status = ProctoredExamStudentAttemptStatus.ready_to_start
exam_attempt_obj.save()
return exam_attempt_obj.id
def remove_exam_attempt_by_id(attempt_id): def remove_exam_attempt_by_id(attempt_id):
""" """
Removes an exam attempt given the attempt id. Removes an exam attempt given the attempt id.
......
...@@ -5,10 +5,9 @@ Various callback paths ...@@ -5,10 +5,9 @@ Various callback paths
from django.template import Context, loader from django.template import Context, loader
from django.http import HttpResponse from django.http import HttpResponse
from edx_proctoring.exceptions import StudentExamAttemptDoesNotExistsException
from edx_proctoring.api import ( from edx_proctoring.api import (
start_exam_attempt_by_code, get_exam_attempt_by_code,
mark_exam_attempt_as_ready,
) )
...@@ -23,15 +22,15 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume ...@@ -23,15 +22,15 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume
as a query string parameter as a query string parameter
""" """
# start the exam! attempt = get_exam_attempt_by_code(attempt_code)
try: if not attempt:
start_exam_attempt_by_code(attempt_code)
except StudentExamAttemptDoesNotExistsException:
return HttpResponse( return HttpResponse(
content='That exam code is not valid', content='That exam code is not valid',
status=404 status=404
) )
mark_exam_attempt_as_ready(attempt['proctored_exam']['id'], attempt['user']['id'])
template = loader.get_template('proctoring/proctoring_launch_callback.html') template = loader.get_template('proctoring/proctoring_launch_callback.html')
return HttpResponse(template.render(Context({}))) return HttpResponse(template.render(Context({})))
...@@ -151,37 +151,37 @@ class ProctoredExamStudentAttemptStatus(object): ...@@ -151,37 +151,37 @@ class ProctoredExamStudentAttemptStatus(object):
""" """
# the student is eligible to decide if he/she wants to persue credit # the student is eligible to decide if he/she wants to persue credit
eligible = 'Eligible' eligible = 'eligible'
# the attempt record has been created, but the exam has not yet # the attempt record has been created, but the exam has not yet
# been started # been started
created = 'Created' created = 'created'
# the attempt is ready to start but requires # the attempt is ready to start but requires
# user to acknowledge that he/she wants to start the exam # user to acknowledge that he/she wants to start the exam
ready_to_start = 'Ready to start' ready_to_start = 'ready_to_start'
# the student has started the exam and is # the student has started the exam and is
# in the process of completing the exam # in the process of completing the exam
started = 'Started' started = 'started'
# the exam has timed out # the exam has timed out
timed_out = 'Timed Out' timed_out = 'timed_out'
# the student has completed the exam # the student has completed the exam
completed = 'Completed' completed = 'completed'
# the student has submitted the exam for proctoring review # the student has submitted the exam for proctoring review
submitted = 'Submitted' submitted = 'submitted'
# the exam has been verified and approved # the exam has been verified and approved
verified = 'Verified' verified = 'verified'
# the exam has been rejected # the exam has been rejected
rejected = 'Rejected' rejected = 'rejected'
# the exam is believed to be in error # the exam is believed to be in error
error = 'Error' error = 'error'
class ProctoredExamStudentAttempt(TimeStampedModel): class ProctoredExamStudentAttempt(TimeStampedModel):
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
time_remaining_seconds: 0, time_remaining_seconds: 0,
low_threshold_sec: 0, low_threshold_sec: 0,
critically_low_threshold_sec: 0, critically_low_threshold_sec: 0,
course_id: null,
lastFetched: new Date() lastFetched: new Date()
}, },
getRemainingSeconds: function () { getRemainingSeconds: function () {
......
...@@ -37,9 +37,12 @@ var edx = edx || {}; ...@@ -37,9 +37,12 @@ var edx = edx || {};
}, },
modelChanged: function () { modelChanged: function () {
// if we are a proctored exam, then we need to alert user that he/she // if we are a proctored exam, then we need to alert user that he/she
// should not leave the exam // should not be navigating around the courseware
var taking_as_proctored = this.model.get('taking_as_proctored');
var time_left = this.model.get('time_remaining_seconds') > 0;
var in_courseware = document.location.href.indexOf('/courses/' + this.model.get('course_id') + '/courseware/') > -1;
if (this.model.get('taking_as_proctored') && this.model.get('time_remaining_seconds') > 0) { if ( taking_as_proctored && time_left && in_courseware){
$(window).bind('beforeunload', this.unloadMessage); $(window).bind('beforeunload', this.unloadMessage);
} else { } else {
// remove callback on unload event // remove callback on unload event
......
...@@ -96,17 +96,28 @@ ...@@ -96,17 +96,28 @@
function poll_exam_started() { function poll_exam_started() {
var url = $('.instructions').data('exam-started-poll-url') var url = $('.instructions').data('exam-started-poll-url')
$.ajax(url).success(function(data){ $.ajax(url).success(function(data){
if (data.started_at !== null) { if (data.status === 'ready_to_start') {
if (_waiting_for_proctored_interval != null) { if (_waiting_for_proctored_interval != null) {
clearInterval(_waiting_for_proctored_interval) clearInterval(_waiting_for_proctored_interval)
} }
// Let the student know exam has started and clock is running. // Let the student know exam is ready to start.
// this may or may not bring the browser window back to the // This alert may or may not bring the browser window back to the
// foreground (depending on browser as well as user settings) // foreground (depending on browser as well as user settings)
alert('{% trans "Your proctored exam has started, please click OK to enter into your exam." %}') alert('{% trans "Your proctored exam has started, please click OK to enter into your exam." %}')
// Reloading page will reflect the new state of the attempt // after the user acknowledges the alert then we can start
location.reload() // the exam and timer
$.ajax({
url: url,
type: 'PUT',
data: {
action: 'start'
},
success: function() {
// Reloading page will reflect the new state of the attempt
location.reload()
}
});
} }
}); });
} }
......
...@@ -28,6 +28,7 @@ from edx_proctoring.api import ( ...@@ -28,6 +28,7 @@ from edx_proctoring.api import (
get_filtered_exam_attempts, get_filtered_exam_attempts,
is_feature_enabled, is_feature_enabled,
mark_exam_attempt_timeout, mark_exam_attempt_timeout,
mark_exam_attempt_as_ready,
) )
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamAlreadyExists,
...@@ -389,6 +390,21 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -389,6 +390,21 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertEqual(proctored_exam_student_attempt.id, proctored_exam_attempt_id) self.assertEqual(proctored_exam_student_attempt.id, proctored_exam_attempt_id)
def test_mark_exam_attempt_as_ready(self):
"""
Tests the mark exam as timed out
"""
with self.assertRaises(StudentExamAttemptDoesNotExistsException):
mark_exam_attempt_as_ready(self.proctored_exam_id, self.user_id)
proctored_exam_student_attempt = self._create_unstarted_exam_attempt()
self.assertIsNone(proctored_exam_student_attempt.completed_at)
proctored_exam_attempt_id = mark_exam_attempt_as_ready(
proctored_exam_student_attempt.proctored_exam, self.user_id
)
self.assertEqual(proctored_exam_student_attempt.id, proctored_exam_attempt_id)
def test_get_active_exams_for_user(self): def test_get_active_exams_for_user(self):
""" """
Test to get the all the active Test to get the all the active
......
...@@ -397,6 +397,53 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -397,6 +397,53 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertGreater(response_data['exam_attempt_id'], 0) self.assertGreater(response_data['exam_attempt_id'], 0)
def test_start_exam(self):
"""
Start an exam (create an exam attempt)
"""
# 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': False,
}
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)
self.assertGreater(response_data['exam_attempt_id'], 0)
old_attempt_id = response_data['exam_attempt_id']
# make sure the exam has not started
attempt = get_exam_attempt_by_id(old_attempt_id)
self.assertIsNone(attempt['started_at'])
response = self.client.put(
reverse('edx_proctoring.proctored_exam.attempt', args=[old_attempt_id]),
{
'action': 'start',
}
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response_data['exam_attempt_id'], old_attempt_id)
# make sure the exam started
attempt = get_exam_attempt_by_id(old_attempt_id)
self.assertIsNotNone(attempt['started_at'])
def test_attempt_readback(self): def test_attempt_readback(self):
""" """
Confirms that an attempt can be read Confirms that an attempt can be read
...@@ -581,7 +628,9 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -581,7 +628,9 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response = self.client.put( response = self.client.put(
reverse('edx_proctoring.proctored_exam.attempt', args=[old_attempt_id]), reverse('edx_proctoring.proctored_exam.attempt', args=[old_attempt_id]),
{} {
'action': 'stop',
}
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -878,7 +927,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -878,7 +927,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
attempt = get_exam_attempt_by_id(attempt_id) attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['started_at']) self.assertEqual(attempt['status'], 'ready_to_start')
def test_bad_exam_code_callback(self): def test_bad_exam_code_callback(self):
""" """
......
...@@ -302,10 +302,18 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -302,10 +302,18 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
) )
raise ProctoredExamPermissionDenied(err_msg) raise ProctoredExamPermissionDenied(err_msg)
exam_attempt_id = stop_exam_attempt( action = request.DATA.get('action')
exam_id=attempt['proctored_exam']['id'],
user_id=request.user.id if action == 'stop':
) exam_attempt_id = stop_exam_attempt(
exam_id=attempt['proctored_exam']['id'],
user_id=request.user.id
)
elif action == 'start':
exam_attempt_id = start_exam_attempt(
exam_id=attempt['proctored_exam']['id'],
user_id=request.user.id
)
return Response({"exam_attempt_id": exam_attempt_id}) return Response({"exam_attempt_id": exam_attempt_id})
except ProctoredBaseException, ex: except ProctoredBaseException, ex:
...@@ -473,6 +481,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView): ...@@ -473,6 +481,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
'time_remaining_seconds': time_remaining_seconds, 'time_remaining_seconds': time_remaining_seconds,
'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'],
} }
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