Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-proctoring
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
edx-proctoring
Commits
17ef189f
Commit
17ef189f
authored
Jul 01, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13 from edx/cdodge/add-proctoring-start-template
Cdodge/add proctoring start template
parents
f3f966c7
137e59f5
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
164 additions
and
65 deletions
+164
-65
edx_proctoring/api.py
+69
-18
edx_proctoring/exceptions.py
+16
-4
edx_proctoring/models.py
+22
-14
edx_proctoring/static/proctoring/js/proctored_exam_model.js
+5
-1
edx_proctoring/static/proctoring/js/proctored_exam_view.js
+3
-1
edx_proctoring/templates/proctoring/seq_proctored_exam_entrance.html
+7
-6
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
+4
-0
edx_proctoring/templates/proctoring/seq_timed_exam_entrance.html
+2
-1
edx_proctoring/tests/test_api.py
+10
-5
edx_proctoring/tests/test_views.py
+6
-4
edx_proctoring/views.py
+20
-11
No files found.
edx_proctoring/api.py
View file @
17ef189f
...
...
@@ -12,13 +12,22 @@ from django.template import Context, loader
from
django.core.urlresolvers
import
reverse
from
edx_proctoring.exceptions
import
(
ProctoredExamAlreadyExists
,
ProctoredExamNotFoundException
,
StudentExamAttemptAlreadyExistsException
,
StudentExamAttemptDoesNotExistsException
)
ProctoredExamAlreadyExists
,
ProctoredExamNotFoundException
,
StudentExamAttemptAlreadyExistsException
,
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptedAlreadyStarted
,
)
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
...
...
@@ -139,31 +148,70 @@ def get_exam_attempt(exam_id, user_id):
"""
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
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
exam_id. If one already exists, then an exception should be thrown.
Returns: exam_attempt_id (PK)
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
start_exam_attempt
(
exam_id
,
user_id
,
external_id
)
if
exam_attempt_obj
is
None
:
raise
StudentExamAttemptAlreadyExistsException
else
:
return
exam_attempt_obj
.
id
existing_attempt
=
ProctoredExamStudentAttempt
.
get_exam_attempt
(
exam_id
,
user_id
)
if
not
existing_attempt
:
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
):
"""
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
:
raise
StudentExamAttemptDoesNotExistsException
raise
StudentExamAttemptDoesNotExistsException
(
'Error. Trying to stop an exam that is not in progress.'
)
else
:
exam_attempt_obj
.
completed_at
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt_obj
.
save
()
...
...
@@ -191,7 +239,7 @@ def get_active_exams_for_user(user_id, course_id=None):
"""
result
=
[]
student_active_exams
=
ProctoredExamStudentAttempt
.
get_active_student_
exam
s
(
user_id
,
course_id
)
student_active_exams
=
ProctoredExamStudentAttempt
.
get_active_student_
attempt
s
(
user_id
,
course_id
)
for
active_exam
in
student_active_exams
:
# convert the django orm objects
# into the serialized form.
...
...
@@ -241,8 +289,8 @@ def get_student_view(user_id, course_id, content_id, context):
)
attempt
=
get_exam_attempt
(
exam_id
,
user_id
)
has_started_exam
=
attempt
is
not
None
if
attempt
:
has_started_exam
=
attempt
and
attempt
.
get
(
'started_at'
)
if
has_started_exam
:
now_utc
=
datetime
.
now
(
pytz
.
UTC
)
expires_at
=
attempt
[
'started_at'
]
+
timedelta
(
minutes
=
context
[
'default_time_limit_mins'
])
has_time_expired
=
now_utc
>
expires_at
...
...
@@ -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
# or a screen regarding proctoring
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
:
student_view_template
=
'proctoring/seq_timed_exam_entrance.html'
elif
has_finished_exam
:
...
...
edx_proctoring/exceptions.py
View file @
17ef189f
...
...
@@ -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.
"""
class
ProctoredExamNotFoundException
(
Exception
):
class
ProctoredExamNotFoundException
(
ProctoredBase
Exception
):
"""
Raised when a look up fails.
"""
class
StudentExamAttemptAlreadyExistsException
(
Exception
):
class
StudentExamAttemptAlreadyExistsException
(
ProctoredBase
Exception
):
"""
Raised when trying to start an exam when an Exam Attempt already exists.
"""
class
StudentExamAttemptDoesNotExistsException
(
Exception
):
class
StudentExamAttemptDoesNotExistsException
(
ProctoredBase
Exception
):
"""
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
"""
edx_proctoring/models.py
View file @
17ef189f
...
...
@@ -89,6 +89,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# in case there is an option to opt-out
taking_as_proctored
=
models
.
BooleanField
()
student_name
=
models
.
CharField
(
max_length
=
255
)
class
Meta
:
""" Meta class for this Django model """
db_table
=
'proctoring_proctoredexamstudentattempt'
...
...
@@ -100,23 +102,29 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return
self
.
started_at
and
not
self
.
completed_at
@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
exam_id. If one already exists, then returns None.
sets the model's state when an exam attempt has started
"""
if
cls
.
get_student_exam_attempt
(
exam_id
,
user_id
)
is
None
:
return
cls
.
objects
.
create
(
proctored_exam_id
=
exam_id
,
user_id
=
user_id
,
external_id
=
external_id
,
started_at
=
datetime
.
now
(
pytz
.
UTC
)
)
else
:
return
None
self
.
started_at
=
datetime
.
now
(
pytz
.
UTC
)
self
.
save
()
@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
else Returns None.
...
...
@@ -128,7 +136,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return
exam_attempt_obj
@classmethod
def
get_active_student_
exam
s
(
cls
,
user_id
,
course_id
=
None
):
def
get_active_student_
attempt
s
(
cls
,
user_id
,
course_id
=
None
):
"""
Returns the active student exams (user in-progress exams)
"""
...
...
edx_proctoring/static/proctoring/js/proctored_exam_model.js
View file @
17ef189f
...
...
@@ -17,10 +17,14 @@
var
currentTime
=
(
new
Date
()).
getTime
();
var
lastFetched
=
this
.
get
(
'lastFetched'
).
getTime
();
var
totalSeconds
=
this
.
get
(
'time_remaining_seconds'
)
-
(
currentTime
-
lastFetched
)
/
1000
;
return
(
totalSeconds
>
0
)
?
totalSeconds
:
0
;
return
totalSeconds
;
},
getFormattedRemainingTime
:
function
()
{
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
minutes
=
parseInt
(
totalSeconds
/
60
)
%
60
;
var
seconds
=
Math
.
floor
(
totalSeconds
%
60
);
...
...
edx_proctoring/static/proctoring/js/proctored_exam_view.js
View file @
17ef189f
...
...
@@ -13,6 +13,8 @@ var edx = edx || {};
this
.
templateId
=
options
.
proctored_template
;
this
.
template
=
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
();
if
(
template_html
!==
null
)
{
...
...
@@ -47,7 +49,7 @@ var edx = edx || {};
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
());
if
(
self
.
model
.
getRemainingSeconds
()
<=
0
)
{
if
(
self
.
model
.
getRemainingSeconds
()
<=
-
self
.
grace_period_secs
)
{
clearInterval
(
self
.
timerId
);
// stop the timer once the time finishes.
// refresh the page when the timer expired
location
.
reload
();
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_entrance.html
View file @
17ef189f
...
...
@@ -15,7 +15,7 @@
</p>
<div
class=
"gated-sequence"
>
<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)" %}
</a>
<p>
...
...
@@ -24,11 +24,11 @@
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>
{% 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
class=
"gated-sequence"
>
<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)" %}
</a>
<p>
...
...
@@ -37,7 +37,7 @@
credit
</strong>
upon completing the exam or this course in general.
{% endblocktrans %}
</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>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
...
...
@@ -47,7 +47,7 @@
function
(
event
)
{
var
action_url
=
$
(
this
).
data
(
'ajax-url'
);
var
exam_id
=
$
(
this
).
data
(
'exam-id'
);
var
choice
=
$
(
this
).
data
(
'choice
'
);
var
attempt_proctored
=
$
(
this
).
data
(
'attempt-proctored
'
);
if
(
typeof
action_url
===
"undefined"
)
{
return
false
;
}
...
...
@@ -55,7 +55,8 @@
action_url
,
{
"exam_id"
:
exam_id
,
"choice"
:
choice
"attempt_proctored"
:
attempt_proctored
,
"start_clock"
:
false
},
function
(
data
)
{
// reload the page, because we've unlocked it
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
0 → 100644
View file @
17ef189f
{% load i18n %}
<div
class=
"sequence"
data-exam-id=
"{{exam_id}}"
>
How to launch the proctored exam content goes here
</div>
edx_proctoring/templates/proctoring/seq_timed_exam_entrance.html
View file @
17ef189f
...
...
@@ -32,7 +32,8 @@
$
.
post
(
action_url
,
{
"exam_id"
:
exam_id
"exam_id"
:
exam_id
,
"start_clock"
:
true
},
function
(
data
)
{
// reload the page, because we've unlocked it
...
...
edx_proctoring/tests/test_api.py
View file @
17ef189f
...
...
@@ -13,7 +13,8 @@ from edx_proctoring.api import (
start_exam_attempt
,
stop_exam_attempt
,
get_active_exams_for_user
,
get_exam_attempt
get_exam_attempt
,
create_exam_attempt
)
from
edx_proctoring.exceptions
import
(
ProctoredExamAlreadyExists
,
...
...
@@ -187,11 +188,11 @@ class ProctoredExamApiTests(LoggedInTestCase):
remove_allowance_for_user
(
student_allowance
.
proctored_exam
.
id
,
self
.
user_id
,
self
.
key
)
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.
"""
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
)
def
test_get_exam_attempt
(
self
):
...
...
@@ -211,7 +212,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
proctored_exam_student_attempt
=
self
.
_create_student_exam_attempt
()
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
):
"""
...
...
@@ -244,11 +245,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_name
=
'Final Test Exam'
,
time_limit_mins
=
self
.
default_time_limit
)
start
_exam_attempt
(
create
_exam_attempt
(
exam_id
=
exam_id
,
user_id
=
self
.
user_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
,
'new_key'
,
'new_value'
)
student_active_exams
=
get_active_exams_for_user
(
self
.
user_id
,
self
.
course_id
)
...
...
edx_proctoring/tests/test_views.py
View file @
17ef189f
...
...
@@ -350,8 +350,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
attempt_data
=
{
'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
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
),
...
...
@@ -376,7 +376,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'user_id'
:
self
.
student_taking_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
}
response
=
self
.
client
.
post
(
...
...
@@ -394,7 +393,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
self
.
assertEqual
(
response
.
status_code
,
400
)
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
):
"""
...
...
edx_proctoring/views.py
View file @
17ef189f
...
...
@@ -19,10 +19,13 @@ from edx_proctoring.api import (
stop_exam_attempt
,
add_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
.utils
import
AuthenticatedAPIView
...
...
@@ -236,20 +239,26 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
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
:
exam_attempt_id
=
start
_exam_attempt
(
exam_id
=
request
.
DATA
.
get
(
'exam_id'
,
None
)
,
exam_attempt_id
=
create
_exam_attempt
(
exam_id
=
exam_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
})
except
StudentExamAttemptAlreadyExistsException
:
except
ProctoredBaseException
,
ex
:
return
Response
(
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
):
...
...
@@ -263,10 +272,10 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
)
return
Response
({
"exam_attempt_id"
:
exam_attempt_id
})
except
StudentExamAttemptDoesNotExistsException
:
except
ProctoredBaseException
,
ex
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"detail"
:
"Error. Trying to stop an exam that is not in progress."
}
data
=
{
"detail"
:
str
(
ex
)
}
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment