Commit 17ef189f by chrisndodge

Merge pull request #13 from edx/cdodge/add-proctoring-start-template

Cdodge/add proctoring start template
parents f3f966c7 137e59f5
...@@ -12,13 +12,22 @@ from django.template import Context, loader ...@@ -12,13 +12,22 @@ from django.template import Context, loader
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamNotFoundException, StudentExamAttemptAlreadyExistsException, ProctoredExamAlreadyExists,
StudentExamAttemptDoesNotExistsException) ProctoredExamNotFoundException,
StudentExamAttemptAlreadyExistsException,
StudentExamAttemptDoesNotExistsException,
StudentExamAttemptedAlreadyStarted,
)
from edx_proctoring.models import ( from edx_proctoring.models import (
ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt ProctoredExam,
ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt,
)
from edx_proctoring.serializers import (
ProctoredExamSerializer,
ProctoredExamStudentAttemptSerializer,
ProctoredExamStudentAllowanceSerializer,
) )
from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer, \
ProctoredExamStudentAllowanceSerializer
from edx_proctoring.utils import humanized_time from edx_proctoring.utils import humanized_time
...@@ -139,31 +148,70 @@ def get_exam_attempt(exam_id, user_id): ...@@ -139,31 +148,70 @@ def get_exam_attempt(exam_id, user_id):
""" """
Return an existing exam attempt for the given student Return an existing exam attempt for the given student
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.get_student_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id)
return exam_attempt_obj.__dict__ if exam_attempt_obj else None return exam_attempt_obj.__dict__ if exam_attempt_obj else None
def start_exam_attempt(exam_id, user_id, external_id): def create_exam_attempt(exam_id, user_id, external_id):
"""
Creates an exam attempt for user_id against exam_id. There should only be
one exam_attempt per user per exam. Multiple attempts by user will be archived
in a separate table
"""
if ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id):
err_msg = (
'Cannot create new exam attempt for exam_id = {exam_id} and '
'user_id = {user_id} because it already exists!'
).format(exam_id=exam_id, user_id=user_id)
raise StudentExamAttemptAlreadyExistsException(err_msg)
attempt = ProctoredExamStudentAttempt.create_exam_attempt(
exam_id,
user_id,
'', # student name is TBD
external_id
)
return attempt.id
def start_exam_attempt(exam_id, user_id):
""" """
Signals the beginning of an exam attempt for a given Signals the beginning of an exam attempt for a given
exam_id. If one already exists, then an exception should be thrown. exam_id. If one already exists, then an exception should be thrown.
Returns: exam_attempt_id (PK) Returns: exam_attempt_id (PK)
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.start_exam_attempt(exam_id, user_id, external_id)
if exam_attempt_obj is None: existing_attempt = ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id)
raise StudentExamAttemptAlreadyExistsException
else: if not existing_attempt:
return exam_attempt_obj.id err_msg = (
'Cannot start exam attempt for exam_id = {exam_id} '
'and user_id = {user_id} because it does not exist!'
).format(exam_id=exam_id, user_id=user_id)
raise StudentExamAttemptDoesNotExistsException(err_msg)
if existing_attempt.started_at:
# cannot restart an attempt
err_msg = (
'Cannot start exam attempt for exam_id = {exam_id} '
'and user_id = {user_id} because it has already started!'
).format(exam_id=exam_id, user_id=user_id)
raise StudentExamAttemptedAlreadyStarted(err_msg)
existing_attempt.start_exam_attempt()
def stop_exam_attempt(exam_id, user_id): def stop_exam_attempt(exam_id, user_id):
""" """
Marks the exam attempt as completed (sets the completed_at field and updates the record) Marks the exam attempt as completed (sets the completed_at field and updates the record)
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.get_student_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None: if exam_attempt_obj is None:
raise StudentExamAttemptDoesNotExistsException raise StudentExamAttemptDoesNotExistsException('Error. Trying to stop an exam that is not in progress.')
else: else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC) exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
exam_attempt_obj.save() exam_attempt_obj.save()
...@@ -191,7 +239,7 @@ def get_active_exams_for_user(user_id, course_id=None): ...@@ -191,7 +239,7 @@ def get_active_exams_for_user(user_id, course_id=None):
""" """
result = [] result = []
student_active_exams = ProctoredExamStudentAttempt.get_active_student_exams(user_id, course_id) student_active_exams = ProctoredExamStudentAttempt.get_active_student_attempts(user_id, course_id)
for active_exam in student_active_exams: for active_exam in student_active_exams:
# convert the django orm objects # convert the django orm objects
# into the serialized form. # into the serialized form.
...@@ -241,8 +289,8 @@ def get_student_view(user_id, course_id, content_id, context): ...@@ -241,8 +289,8 @@ def get_student_view(user_id, course_id, content_id, context):
) )
attempt = get_exam_attempt(exam_id, user_id) attempt = get_exam_attempt(exam_id, user_id)
has_started_exam = attempt is not None has_started_exam = attempt and attempt.get('started_at')
if attempt: if has_started_exam:
now_utc = datetime.now(pytz.UTC) now_utc = datetime.now(pytz.UTC)
expires_at = attempt['started_at'] + timedelta(minutes=context['default_time_limit_mins']) expires_at = attempt['started_at'] + timedelta(minutes=context['default_time_limit_mins'])
has_time_expired = now_utc > expires_at has_time_expired = now_utc > expires_at
...@@ -251,7 +299,10 @@ def get_student_view(user_id, course_id, content_id, context): ...@@ -251,7 +299,10 @@ def get_student_view(user_id, course_id, content_id, context):
# 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
if is_proctored: if is_proctored:
student_view_template = 'proctoring/seq_proctored_exam_entrance.html' if not attempt:
student_view_template = 'proctoring/seq_proctored_exam_entrance.html'
else:
student_view_template = 'proctoring/seq_proctored_exam_instructions.html'
else: else:
student_view_template = 'proctoring/seq_timed_exam_entrance.html' student_view_template = 'proctoring/seq_timed_exam_entrance.html'
elif has_finished_exam: elif has_finished_exam:
......
...@@ -3,25 +3,37 @@ Specialized exceptions for the Notification subsystem ...@@ -3,25 +3,37 @@ Specialized exceptions for the Notification subsystem
""" """
class ProctoredExamAlreadyExists(Exception): class ProctoredBaseException(Exception):
"""
A common base class for all exceptions
"""
class ProctoredExamAlreadyExists(ProctoredBaseException):
""" """
Raised when trying to create an Exam that already exists. Raised when trying to create an Exam that already exists.
""" """
class ProctoredExamNotFoundException(Exception): class ProctoredExamNotFoundException(ProctoredBaseException):
""" """
Raised when a look up fails. Raised when a look up fails.
""" """
class StudentExamAttemptAlreadyExistsException(Exception): class StudentExamAttemptAlreadyExistsException(ProctoredBaseException):
""" """
Raised when trying to start an exam when an Exam Attempt already exists. Raised when trying to start an exam when an Exam Attempt already exists.
""" """
class StudentExamAttemptDoesNotExistsException(Exception): class StudentExamAttemptDoesNotExistsException(ProctoredBaseException):
""" """
Raised when trying to stop an exam attempt where the Exam Attempt doesn't exist. Raised when trying to stop an exam attempt where the Exam Attempt doesn't exist.
""" """
class StudentExamAttemptedAlreadyStarted(ProctoredBaseException):
"""
Raised when the same exam attempt is being started twice
"""
...@@ -89,6 +89,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -89,6 +89,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# in case there is an option to opt-out # in case there is an option to opt-out
taking_as_proctored = models.BooleanField() taking_as_proctored = models.BooleanField()
student_name = models.CharField(max_length=255)
class Meta: class Meta:
""" Meta class for this Django model """ """ Meta class for this Django model """
db_table = 'proctoring_proctoredexamstudentattempt' db_table = 'proctoring_proctoredexamstudentattempt'
...@@ -100,23 +102,29 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -100,23 +102,29 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return self.started_at and not self.completed_at return self.started_at and not self.completed_at
@classmethod @classmethod
def start_exam_attempt(cls, exam_id, user_id, external_id): def create_exam_attempt(cls, exam_id, user_id, student_name, external_id):
"""
Create a new exam attempt entry for a given exam_id and
user_id.
"""
return cls.objects.create(
proctored_exam_id=exam_id,
user_id=user_id,
student_name=student_name,
external_id=external_id
)
def start_exam_attempt(self):
""" """
create and return an exam attempt entry for a given sets the model's state when an exam attempt has started
exam_id. If one already exists, then returns None.
""" """
if cls.get_student_exam_attempt(exam_id, user_id) is None:
return cls.objects.create( self.started_at = datetime.now(pytz.UTC)
proctored_exam_id=exam_id, self.save()
user_id=user_id,
external_id=external_id,
started_at=datetime.now(pytz.UTC)
)
else:
return None
@classmethod @classmethod
def get_student_exam_attempt(cls, exam_id, user_id): def get_exam_attempt(cls, exam_id, user_id):
""" """
Returns the Student Exam Attempt object if found Returns the Student Exam Attempt object if found
else Returns None. else Returns None.
...@@ -128,7 +136,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -128,7 +136,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return exam_attempt_obj return exam_attempt_obj
@classmethod @classmethod
def get_active_student_exams(cls, user_id, course_id=None): def get_active_student_attempts(cls, user_id, course_id=None):
""" """
Returns the active student exams (user in-progress exams) Returns the active student exams (user in-progress exams)
""" """
......
...@@ -17,10 +17,14 @@ ...@@ -17,10 +17,14 @@
var currentTime = (new Date()).getTime(); var currentTime = (new Date()).getTime();
var lastFetched = this.get('lastFetched').getTime(); var lastFetched = this.get('lastFetched').getTime();
var totalSeconds = this.get('time_remaining_seconds') - (currentTime - lastFetched) / 1000; var totalSeconds = this.get('time_remaining_seconds') - (currentTime - lastFetched) / 1000;
return (totalSeconds > 0) ? totalSeconds : 0; return totalSeconds;
}, },
getFormattedRemainingTime: function () { getFormattedRemainingTime: function () {
var totalSeconds = this.getRemainingSeconds(); var totalSeconds = this.getRemainingSeconds();
/* since we can have a small grace period, we can end in the negative numbers */
if (totalSeconds < 0)
totalSeconds = 0;
var hours = parseInt(totalSeconds / 3600) % 24; var hours = parseInt(totalSeconds / 3600) % 24;
var minutes = parseInt(totalSeconds / 60) % 60; var minutes = parseInt(totalSeconds / 60) % 60;
var seconds = Math.floor(totalSeconds % 60); var seconds = Math.floor(totalSeconds % 60);
......
...@@ -13,6 +13,8 @@ var edx = edx || {}; ...@@ -13,6 +13,8 @@ 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;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this.grace_period_secs = 5;
var template_html = $(this.templateId).text(); var template_html = $(this.templateId).text();
if (template_html !== null) { if (template_html !== null) {
...@@ -47,7 +49,7 @@ var edx = edx || {}; ...@@ -47,7 +49,7 @@ var edx = edx || {};
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());
if (self.model.getRemainingSeconds() <= 0) { if (self.model.getRemainingSeconds() <= -self.grace_period_secs) {
clearInterval(self.timerId); // stop the timer once the time finishes. clearInterval(self.timerId); // stop the timer once the time finishes.
// refresh the page when the timer expired // refresh the page when the timer expired
location.reload(); location.reload();
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
</p> </p>
<div class="gated-sequence"> <div class="gated-sequence">
<span><i class="fa fa-lock"></i></span> <span><i class="fa fa-lock"></i></span>
<a class="start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-choice="proctored"> <a class="start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true>
{% trans "Yes, take this exam as a proctored exam (and be eligible for credit)" %} {% trans "Yes, take this exam as a proctored exam (and be eligible for credit)" %}
</a> </a>
<p> <p>
...@@ -24,11 +24,11 @@ ...@@ -24,11 +24,11 @@
of your exam. After successful installation, you will be <strong>guided through setting up your of your exam. After successful installation, you will be <strong>guided through setting up your
proctored session and begin the exam immediately </strong>afterwards.</p> proctored session and begin the exam immediately </strong>afterwards.</p>
{% endblocktrans %} {% endblocktrans %}
<i class="fa fa-arrow-circle-right start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-choice="proctored"></i> <i class="fa fa-arrow-circle-right start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true></i>
</div> </div>
<div class="gated-sequence"> <div class="gated-sequence">
<span><i class="fa fa-unlock"></i></span> <span><i class="fa fa-unlock"></i></span>
<a class="start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-choice="unproctored"> <a class="start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=false>
{% trans "No, take this exam as an open exam (and not be eligible for credit)" %} {% trans "No, take this exam as an open exam (and not be eligible for credit)" %}
</a> </a>
<p> <p>
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
credit </strong>upon completing the exam or this course in general. credit </strong>upon completing the exam or this course in general.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<i class="fa fa-arrow-circle-right start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-choice="unproctored"></i> <i class="fa fa-arrow-circle-right start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=false></i>
</div> </div>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctoring/seq_proctored_exam_footer.html' %}
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
function(event) { function(event) {
var action_url = $(this).data('ajax-url'); var action_url = $(this).data('ajax-url');
var exam_id = $(this).data('exam-id'); var exam_id = $(this).data('exam-id');
var choice = $(this).data('choice'); var attempt_proctored = $(this).data('attempt-proctored');
if (typeof action_url === "undefined" ) { if (typeof action_url === "undefined" ) {
return false; return false;
} }
...@@ -55,7 +55,8 @@ ...@@ -55,7 +55,8 @@
action_url, action_url,
{ {
"exam_id": exam_id, "exam_id": exam_id,
"choice": choice "attempt_proctored": attempt_proctored,
"start_clock": false
}, },
function(data) { function(data) {
// reload the page, because we've unlocked it // reload the page, because we've unlocked it
......
{% load i18n %}
<div class="sequence" data-exam-id="{{exam_id}}">
How to launch the proctored exam content goes here
</div>
...@@ -32,7 +32,8 @@ ...@@ -32,7 +32,8 @@
$.post( $.post(
action_url, action_url,
{ {
"exam_id": exam_id "exam_id": exam_id,
"start_clock": true
}, },
function(data) { function(data) {
// reload the page, because we've unlocked it // reload the page, because we've unlocked it
......
...@@ -13,7 +13,8 @@ from edx_proctoring.api import ( ...@@ -13,7 +13,8 @@ from edx_proctoring.api import (
start_exam_attempt, start_exam_attempt,
stop_exam_attempt, stop_exam_attempt,
get_active_exams_for_user, get_active_exams_for_user,
get_exam_attempt get_exam_attempt,
create_exam_attempt
) )
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamAlreadyExists,
...@@ -187,11 +188,11 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -187,11 +188,11 @@ class ProctoredExamApiTests(LoggedInTestCase):
remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key) remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key)
self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 0) self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 0)
def test_start_an_exam_attempt(self): def test_create_an_exam_attempt(self):
""" """
Start an exam attempt. Start an exam attempt.
""" """
attempt_id = start_exam_attempt(self.proctored_exam_id, self.user_id, self.external_id) attempt_id = create_exam_attempt(self.proctored_exam_id, self.user_id, '')
self.assertGreater(attempt_id, 0) self.assertGreater(attempt_id, 0)
def test_get_exam_attempt(self): def test_get_exam_attempt(self):
...@@ -211,7 +212,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -211,7 +212,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
proctored_exam_student_attempt = self._create_student_exam_attempt() proctored_exam_student_attempt = self._create_student_exam_attempt()
with self.assertRaises(StudentExamAttemptAlreadyExistsException): with self.assertRaises(StudentExamAttemptAlreadyExistsException):
start_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id, self.external_id) create_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id, self.external_id)
def test_stop_exam_attempt(self): def test_stop_exam_attempt(self):
""" """
...@@ -244,11 +245,15 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -244,11 +245,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_name='Final Test Exam', exam_name='Final Test Exam',
time_limit_mins=self.default_time_limit time_limit_mins=self.default_time_limit
) )
start_exam_attempt( create_exam_attempt(
exam_id=exam_id, exam_id=exam_id,
user_id=self.user_id, user_id=self.user_id,
external_id=self.external_id external_id=self.external_id
) )
start_exam_attempt(
exam_id=exam_id,
user_id=self.user_id,
)
add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value) add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value)
add_allowance_for_user(self.proctored_exam_id, self.user_id, 'new_key', 'new_value') add_allowance_for_user(self.proctored_exam_id, self.user_id, 'new_key', 'new_value')
student_active_exams = get_active_exams_for_user(self.user_id, self.course_id) student_active_exams = get_active_exams_for_user(self.user_id, self.course_id)
......
...@@ -350,8 +350,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -350,8 +350,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
) )
attempt_data = { attempt_data = {
'exam_id': proctored_exam.id, 'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id, 'external_id': proctored_exam.external_id,
'external_id': proctored_exam.external_id 'start_clock': True,
} }
response = self.client.post( response = self.client.post(
reverse('edx_proctoring.proctored_exam.attempt'), reverse('edx_proctoring.proctored_exam.attempt'),
...@@ -376,7 +376,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -376,7 +376,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
) )
attempt_data = { attempt_data = {
'exam_id': proctored_exam.id, 'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id,
'external_id': proctored_exam.external_id 'external_id': proctored_exam.external_id
} }
response = self.client.post( response = self.client.post(
...@@ -394,7 +393,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -394,7 +393,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data['detail'], 'Error. Trying to start an exam that has already started.') self.assertEqual(
response_data['detail'],
'Cannot create new exam attempt for exam_id = 1 and user_id = 1 because it already exists!'
)
def test_stop_exam_attempt(self): def test_stop_exam_attempt(self):
""" """
......
...@@ -19,10 +19,13 @@ from edx_proctoring.api import ( ...@@ -19,10 +19,13 @@ from edx_proctoring.api import (
stop_exam_attempt, stop_exam_attempt,
add_allowance_for_user, add_allowance_for_user,
remove_allowance_for_user, remove_allowance_for_user,
get_active_exams_for_user get_active_exams_for_user,
create_exam_attempt
)
from edx_proctoring.exceptions import (
ProctoredBaseException,
ProctoredExamNotFoundException,
) )
from edx_proctoring.exceptions import ProctoredExamNotFoundException, \
StudentExamAttemptAlreadyExistsException, StudentExamAttemptDoesNotExistsException
from edx_proctoring.serializers import ProctoredExamSerializer from edx_proctoring.serializers import ProctoredExamSerializer
from .utils import AuthenticatedAPIView from .utils import AuthenticatedAPIView
...@@ -236,20 +239,26 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -236,20 +239,26 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
def post(self, request): def post(self, request):
""" """
HTTP POST handler. To start an exam. HTTP POST handler. To create an exam.
""" """
start_immediately = request.DATA.get('start_clock', 'false').lower() == 'true'
exam_id = request.DATA.get('exam_id', None)
try: try:
exam_attempt_id = start_exam_attempt( exam_attempt_id = create_exam_attempt(
exam_id=request.DATA.get('exam_id', None), exam_id=exam_id,
user_id=request.user.id, user_id=request.user.id,
external_id=request.DATA.get('external_id', None) external_id=request.DATA.get('external_id', None),
) )
if start_immediately:
start_exam_attempt(exam_id, request.user.id)
return Response({'exam_attempt_id': exam_attempt_id}) return Response({'exam_attempt_id': exam_attempt_id})
except StudentExamAttemptAlreadyExistsException: except ProctoredBaseException, ex:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "Error. Trying to start an exam that has already started."} data={"detail": str(ex)}
) )
def put(self, request): def put(self, request):
...@@ -263,10 +272,10 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -263,10 +272,10 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
) )
return Response({"exam_attempt_id": exam_attempt_id}) return Response({"exam_attempt_id": exam_attempt_id})
except StudentExamAttemptDoesNotExistsException: except ProctoredBaseException, ex:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "Error. Trying to stop an exam that is not in progress."} data={"detail": str(ex)}
) )
......
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