Commit 6e7b4dba by chrisndodge

Merge pull request #29 from edx/cdodge/punchlist2

Add status to attempts
parents d7d6afe8 40f36c45
...@@ -24,6 +24,7 @@ from edx_proctoring.models import ( ...@@ -24,6 +24,7 @@ from edx_proctoring.models import (
ProctoredExam, ProctoredExam,
ProctoredExamStudentAllowance, ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt, ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
) )
from edx_proctoring.serializers import ( from edx_proctoring.serializers import (
ProctoredExamSerializer, ProctoredExamSerializer,
...@@ -182,6 +183,17 @@ def get_exam_attempt_by_id(attempt_id): ...@@ -182,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
...@@ -211,7 +223,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -211,7 +223,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
allowance_extra_mins = int(allowance.value) allowance_extra_mins = int(allowance.value)
allowed_time_limit_mins += allowance_extra_mins allowed_time_limit_mins += allowance_extra_mins
attempt_code = unicode(uuid.uuid4()) attempt_code = unicode(uuid.uuid4()).upper()
external_id = None external_id = None
if taking_as_proctored: if taking_as_proctored:
...@@ -263,7 +275,7 @@ def start_exam_attempt(exam_id, user_id): ...@@ -263,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):
...@@ -282,7 +294,7 @@ def start_exam_attempt_by_code(attempt_code): ...@@ -282,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):
...@@ -301,6 +313,8 @@ def _start_exam_attempt(existing_attempt): ...@@ -301,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):
""" """
...@@ -308,9 +322,37 @@ def stop_exam_attempt(exam_id, user_id): ...@@ -308,9 +322,37 @@ def stop_exam_attempt(exam_id, user_id):
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None: if exam_attempt_obj is None:
raise StudentExamAttemptDoesNotExistsException('Error. Trying to stop an exam that is not in progress.') raise StudentExamAttemptDoesNotExistsException('Error. Trying to stop an exam that does not exist.')
else: else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC) exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
exam_attempt_obj.status = ProctoredExamStudentAttemptStatus.completed
exam_attempt_obj.save()
return exam_attempt_obj.id
def mark_exam_attempt_timeout(exam_id, user_id):
"""
Marks the exam attempt as timed_out
"""
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.timed_out
exam_attempt_obj.save()
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() exam_attempt_obj.save()
return exam_attempt_obj.id return exam_attempt_obj.id
...@@ -419,7 +461,7 @@ def get_active_exams_for_user(user_id, course_id=None): ...@@ -419,7 +461,7 @@ def get_active_exams_for_user(user_id, course_id=None):
return result return result
def get_student_view(user_id, course_id, content_id, context): def get_student_view(user_id, course_id, content_id, context): # pylint: disable=too-many-branches
""" """
Helper method that will return the view HTML related to the exam control Helper method that will return the view HTML related to the exam control
flow (i.e. entering, expired, completed, etc.) If there is no specific flow (i.e. entering, expired, completed, etc.) If there is no specific
...@@ -463,6 +505,10 @@ def get_student_view(user_id, course_id, content_id, context): ...@@ -463,6 +505,10 @@ def get_student_view(user_id, course_id, content_id, context):
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins']) expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
has_time_expired = now_utc > expires_at 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:
mark_exam_attempt_timeout(exam_id, user_id)
if not has_started_exam: if not has_started_exam:
# determine whether to show a timed exam only entrance screen # determine whether to show a timed exam only entrance screen
# or a screen regarding proctoring # or a screen regarding proctoring
...@@ -483,7 +529,6 @@ def get_student_view(user_id, course_id, content_id, context): ...@@ -483,7 +529,6 @@ def get_student_view(user_id, course_id, content_id, context):
student_view_template = 'proctoring/seq_timed_exam_completed.html' student_view_template = 'proctoring/seq_timed_exam_completed.html'
elif has_time_expired: elif has_time_expired:
student_view_template = 'proctoring/seq_timed_exam_expired.html' student_view_template = 'proctoring/seq_timed_exam_expired.html'
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)
......
...@@ -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({})))
...@@ -141,6 +141,49 @@ class ProctoredExamStudentAttemptManager(models.Manager): ...@@ -141,6 +141,49 @@ class ProctoredExamStudentAttemptManager(models.Manager):
return self.filter(filtered_query) return self.filter(filtered_query)
class ProctoredExamStudentAttemptStatus(object):
"""
A class to enumerate the various status that an attempt can have
IMPORTANT: Since these values are stored in a database, they are system
constants and should not be language translated, since translations
might change over time.
"""
# the student is eligible to decide if he/she wants to persue credit
eligible = 'eligible'
# the attempt record has been created, but the exam has not yet
# been started
created = 'created'
# the attempt is ready to start but requires
# user to acknowledge that he/she wants to start the exam
ready_to_start = 'ready_to_start'
# the student has started the exam and is
# in the process of completing the exam
started = 'started'
# the exam has timed out
timed_out = 'timed_out'
# the student has completed the exam
completed = 'completed'
# the student has submitted the exam for proctoring review
submitted = 'submitted'
# the exam has been verified and approved
verified = 'verified'
# the exam has been rejected
rejected = 'rejected'
# the exam is believed to be in error
error = 'error'
class ProctoredExamStudentAttempt(TimeStampedModel): class ProctoredExamStudentAttempt(TimeStampedModel):
""" """
Information about the Student Attempt on a Information about the Student Attempt on a
...@@ -205,7 +248,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -205,7 +248,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
attempt_code=attempt_code, attempt_code=attempt_code,
taking_as_proctored=taking_as_proctored, taking_as_proctored=taking_as_proctored,
is_sample_attempt=is_sample_attempt, is_sample_attempt=is_sample_attempt,
external_id=external_id external_id=external_id,
status=ProctoredExamStudentAttemptStatus.created,
) )
def start_exam_attempt(self): def start_exam_attempt(self):
...@@ -213,6 +257,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -213,6 +257,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
sets the model's state when an exam attempt has started sets the model's state when an exam attempt has started
""" """
self.started_at = datetime.now(pytz.UTC) self.started_at = datetime.now(pytz.UTC)
self.status = ProctoredExamStudentAttemptStatus.started
self.save() self.save()
def delete_exam_attempt(self): def delete_exam_attempt(self):
......
...@@ -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 () {
......
var edx = edx || {}; var edx = edx || {};
(function (Backbone, $, _) { (function (Backbone, $, _, gettext) {
'use strict'; 'use strict';
edx.instructor_dashboard = edx.instructor_dashboard || {}; edx.instructor_dashboard = edx.instructor_dashboard || {};
...@@ -11,7 +11,7 @@ var edx = edx || {}; ...@@ -11,7 +11,7 @@ var edx = edx || {};
return new Date(date).toString('MMM dd, yyyy h:mmtt'); return new Date(date).toString('MMM dd, yyyy h:mmtt');
} }
else { else {
return 'N/A'; return '---';
} }
} }
...@@ -130,6 +130,11 @@ var edx = edx || {}; ...@@ -130,6 +130,11 @@ var edx = edx || {};
}, },
onRemoveAttempt: function (event) { onRemoveAttempt: function (event) {
event.preventDefault(); event.preventDefault();
// confirm the user's intent
if (!confirm(gettext('Are you sure you wish to remove this student\'s exam attempt?'))) {
return;
}
var $target = $(event.currentTarget); var $target = $(event.currentTarget);
var attemptId = $target.data("attemptId"); var attemptId = $target.data("attemptId");
...@@ -148,4 +153,4 @@ var edx = edx || {}; ...@@ -148,4 +153,4 @@ var edx = edx || {};
} }
}); });
this.edx.instructor_dashboard.proctoring.ProctoredExamAttemptView = edx.instructor_dashboard.proctoring.ProctoredExamAttemptView; this.edx.instructor_dashboard.proctoring.ProctoredExamAttemptView = edx.instructor_dashboard.proctoring.ProctoredExamAttemptView;
}).call(this, Backbone, $, _); }).call(this, Backbone, $, _, gettext);
var edx = edx || {}; var edx = edx || {};
(function (Backbone, $, _) { (function (Backbone, $, _, gettext) {
'use strict'; 'use strict';
edx.coursware = edx.coursware || {}; edx.coursware = edx.coursware || {};
...@@ -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
...@@ -61,11 +64,11 @@ var edx = edx || {}; ...@@ -61,11 +64,11 @@ var edx = edx || {};
return this; return this;
}, },
unloadMessage: function () { unloadMessage: function () {
return "As you are currently taking a proctored exam,\n" + return gettext("As you are currently taking a proctored exam,\n" +
"you should not be navigation away from the exam.\n" + "you should not be navigation away from the exam.\n" +
"This may be considered as a violation of the \n" + "This may be considered as a violation of the \n" +
"proctored exam and you may be disqualified for \n" + "proctored exam and you may be disqualified for \n" +
"credit eligibility in this course.\n"; "credit eligibility in this course.\n");
}, },
updateRemainingTime: function (self) { updateRemainingTime: function (self) {
self.$el.find('div.exam-timer').removeClass("low-time warning critical"); self.$el.find('div.exam-timer').removeClass("low-time warning critical");
...@@ -80,4 +83,4 @@ var edx = edx || {}; ...@@ -80,4 +83,4 @@ var edx = edx || {};
} }
}); });
this.edx.coursware.proctored_exam.ProctoredExamView = edx.coursware.proctored_exam.ProctoredExamView; this.edx.coursware.proctored_exam.ProctoredExamView = edx.coursware.proctored_exam.ProctoredExamView;
}).call(this, Backbone, $, _); }).call(this, Backbone, $, _, gettext);
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<section class="content"> <section class="content">
<div class="top-header"> <div class="top-header">
<div class='search-attempts'> <div class='search-attempts'>
<input type="text" id="search_attempt_id" placeholder="e.g johndoe or john.do@gmail.com" <input type="text" id="search_attempt_id" placeholder="e.g johndoe or john.doe@gmail.com"
<% if (inSearchMode) { %> <% if (inSearchMode) { %>
value="<%= searchText %>" value="<%= searchText %>"
<%} %> <%} %>
...@@ -118,4 +118,4 @@ ...@@ -118,4 +118,4 @@
</tbody> </tbody>
</table> </table>
</section> </section>
</div> </div>
\ No newline at end of file
...@@ -42,6 +42,8 @@ ...@@ -42,6 +42,8 @@
<script type="text/javascript"> <script type="text/javascript">
var _waiting_for_proctored_interval = null;
$(document).ready(function(){ $(document).ready(function(){
var hasFlash = false; var hasFlash = false;
...@@ -85,7 +87,7 @@ ...@@ -85,7 +87,7 @@
); );
} }
setInterval( _waiting_for_proctored_interval = setInterval(
poll_exam_started, poll_exam_started,
5000 5000
); );
...@@ -94,14 +96,28 @@ ...@@ -94,14 +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') {
// Let the student know exam has started and clock is running. if (_waiting_for_proctored_interval != null) {
// this may or may not bring the browser window back to the clearInterval(_waiting_for_proctored_interval)
}
// Let the student know exam is ready to start.
// 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()
}
});
} }
}); });
} }
......
...@@ -27,6 +27,8 @@ from edx_proctoring.api import ( ...@@ -27,6 +27,8 @@ from edx_proctoring.api import (
get_all_exam_attempts, get_all_exam_attempts,
get_filtered_exam_attempts, get_filtered_exam_attempts,
is_feature_enabled, is_feature_enabled,
mark_exam_attempt_timeout,
mark_exam_attempt_as_ready,
) )
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamAlreadyExists,
...@@ -373,6 +375,36 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -373,6 +375,36 @@ class ProctoredExamApiTests(LoggedInTestCase):
with self.assertRaises(StudentExamAttemptDoesNotExistsException): with self.assertRaises(StudentExamAttemptDoesNotExistsException):
stop_exam_attempt(self.proctored_exam_id, self.user_id) stop_exam_attempt(self.proctored_exam_id, self.user_id)
def test_mark_exam_attempt_timeout(self):
"""
Tests the mark exam as timed out
"""
with self.assertRaises(StudentExamAttemptDoesNotExistsException):
mark_exam_attempt_timeout(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_timeout(
proctored_exam_student_attempt.proctored_exam, self.user_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