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
edx
edx-proctoring
Commits
a8ba1176
Commit
a8ba1176
authored
Aug 04, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #57 from edx/cdodge/complete-transition
make status transitions to completed more formalized
parents
38ee5d8f
9f46dc33
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
311 additions
and
90 deletions
+311
-90
edx_proctoring/api.py
+70
-10
edx_proctoring/exceptions.py
+6
-0
edx_proctoring/models.py
+57
-65
edx_proctoring/templates/proctoring/proctoring_launch_callback.html
+2
-2
edx_proctoring/templates/proctoring/seq_proctored_exam_ready_to_submit.html
+38
-5
edx_proctoring/templates/proctoring/seq_timed_exam_ready_to_submit.html
+0
-0
edx_proctoring/tests/test_api.py
+59
-5
edx_proctoring/tests/test_views.py
+67
-1
edx_proctoring/views.py
+12
-2
No files found.
edx_proctoring/api.py
View file @
a8ba1176
...
...
@@ -18,6 +18,7 @@ from edx_proctoring.exceptions import (
StudentExamAttemptAlreadyExistsException
,
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptedAlreadyStarted
,
ProctoredExamIllegalStatusTransition
,
)
from
edx_proctoring.models
import
(
ProctoredExam
,
...
...
@@ -373,7 +374,7 @@ def _start_exam_attempt(existing_attempt):
Helper method
"""
if
existing_attempt
.
started_at
:
if
existing_attempt
.
started_at
and
existing_attempt
.
status
==
ProctoredExamStudentAttemptStatus
.
started
:
# cannot restart an attempt
err_msg
=
(
'Cannot start exam attempt for exam_id = {exam_id} '
...
...
@@ -382,7 +383,11 @@ def _start_exam_attempt(existing_attempt):
raise
StudentExamAttemptedAlreadyStarted
(
err_msg
)
existing_attempt
.
start_exam_attempt
()
update_attempt_status
(
existing_attempt
.
proctored_exam_id
,
existing_attempt
.
user_id
,
ProctoredExamStudentAttemptStatus
.
started
)
return
existing_attempt
.
id
...
...
@@ -391,7 +396,7 @@ def stop_exam_attempt(exam_id, user_id):
"""
Marks the exam attempt as completed (sets the completed_at field and updates the record)
"""
return
update_attempt_status
(
exam_id
,
user_id
,
ProctoredExamStudentAttemptStatus
.
completed
)
return
update_attempt_status
(
exam_id
,
user_id
,
ProctoredExamStudentAttemptStatus
.
ready_to_submit
)
def
mark_exam_attempt_timeout
(
exam_id
,
user_id
):
...
...
@@ -417,6 +422,37 @@ def update_attempt_status(exam_id, user_id, to_status):
if
exam_attempt_obj
is
None
:
raise
StudentExamAttemptDoesNotExistsException
(
'Error. Trying to look up an exam that does not exist.'
)
#
# don't allow state transitions from a completed state to an incomplete state
# if a re-attempt is desired then the current attempt must be deleted
#
in_completed_status
=
exam_attempt_obj
.
status
in
[
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
declined
,
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
error
,
ProctoredExamStudentAttemptStatus
.
timed_out
]
to_incompleted_status
=
to_status
in
[
ProctoredExamStudentAttemptStatus
.
eligible
,
ProctoredExamStudentAttemptStatus
.
created
,
ProctoredExamStudentAttemptStatus
.
ready_to_start
,
ProctoredExamStudentAttemptStatus
.
started
,
ProctoredExamStudentAttemptStatus
.
ready_to_submit
]
if
in_completed_status
and
to_incompleted_status
:
err_msg
=
(
'A status transition from {from_status} to {to_status} was attempted '
'on exam_id {exam_id} for user_id {user_id}. This is not '
'allowed!'
.
format
(
from_status
=
exam_attempt_obj
.
status
,
to_status
=
to_status
,
exam_id
=
exam_id
,
user_id
=
user_id
)
)
raise
ProctoredExamIllegalStatusTransition
(
err_msg
)
# OK, state transition is fine, we can proceed
exam_attempt_obj
.
status
=
to_status
exam_attempt_obj
.
save
()
...
...
@@ -427,7 +463,7 @@ def update_attempt_status(exam_id, user_id, to_status):
update_credit
=
to_status
in
[
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
declined
,
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
submitted
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
error
]
if
update_credit
:
...
...
@@ -446,6 +482,22 @@ def update_attempt_status(exam_id, user_id, to_status):
status
=
verification
)
if
to_status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
# also mark the exam attempt completed_at timestamp
# after we submit the attempt
exam_attempt_obj
.
completed_at
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt_obj
.
save
()
# if we have transitioned to started and haven't set our
# started_at timestamp, do so now
add_start_time
=
(
to_status
==
ProctoredExamStudentAttemptStatus
.
started
and
not
exam_attempt_obj
.
started_at
)
if
add_start_time
:
exam_attempt_obj
.
started_at
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt_obj
.
save
()
return
exam_attempt_obj
.
id
...
...
@@ -646,10 +698,11 @@ def get_student_view(user_id, course_id, content_id,
if
attempt
and
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
declined
:
return
None
does_time_remain
=
False
has_started_exam
=
attempt
and
attempt
.
get
(
'started_at'
)
if
has_started_exam
:
if
attempt
.
get
(
'status'
)
==
'error'
:
student_view_template
=
'proctoring/seq_proctored_exam_error.html'
expires_at
=
attempt
[
'started_at'
]
+
timedelta
(
minutes
=
attempt
[
'allowed_time_limit_mins'
])
does_time_remain
=
datetime
.
now
(
pytz
.
UTC
)
<
expires_at
if
not
has_started_exam
:
# determine whether to show a timed exam only entrance screen
...
...
@@ -670,6 +723,8 @@ def get_student_view(user_id, course_id, content_id,
})
else
:
student_view_template
=
'proctoring/seq_timed_exam_entrance.html'
elif
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
error
:
student_view_template
=
'proctoring/seq_proctored_exam_error.html'
elif
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
timed_out
:
student_view_template
=
'proctoring/seq_timed_exam_expired.html'
elif
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
submitted
:
...
...
@@ -681,11 +736,11 @@ def get_student_view(user_id, course_id, content_id,
student_view_template
=
'proctoring/seq_proctored_exam_verified.html'
elif
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
rejected
:
student_view_template
=
'proctoring/seq_proctored_exam_rejected.html'
elif
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
completed
:
elif
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
ready_to_submit
:
if
is_proctored
:
student_view_template
=
'proctoring/seq_proctored_exam_
completed
.html'
student_view_template
=
'proctoring/seq_proctored_exam_
ready_to_submit
.html'
else
:
student_view_template
=
'proctoring/seq_timed_exam_
completed
.html'
student_view_template
=
'proctoring/seq_timed_exam_
ready_to_submit
.html'
if
student_view_template
:
template
=
loader
.
get_template
(
student_view_template
)
...
...
@@ -706,11 +761,16 @@ def get_student_view(user_id, course_id, content_id,
'exam_id'
:
exam_id
,
'progress_page_url'
:
progress_page_url
,
'is_sample_attempt'
:
attempt
[
'is_sample_attempt'
]
if
attempt
else
False
,
'does_time_remain'
:
does_time_remain
,
'enter_exam_endpoint'
:
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
'exam_started_poll_url'
:
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt
[
'id'
]]
)
if
attempt
else
''
)
if
attempt
else
''
,
'change_state_url'
:
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt
[
'id'
]]
)
if
attempt
else
''
,
})
return
template
.
render
(
django_context
)
...
...
edx_proctoring/exceptions.py
View file @
a8ba1176
...
...
@@ -75,3 +75,9 @@ class ProctoredExamBadReviewStatus(ProctoredBaseException):
"""
Raised if we get an unexpected status back from the Proctoring attempt review status
"""
class
ProctoredExamIllegalStatusTransition
(
ProctoredBaseException
):
"""
Raised if a state transition is not allowed, e.g. going from submitted to started
"""
edx_proctoring/models.py
View file @
a8ba1176
"""
Data models for the proctoring subsystem
"""
import
pytz
from
datetime
import
datetime
from
django.db
import
models
from
django.db.models
import
Q
from
django.db.models.signals
import
post_save
,
pre_delete
...
...
@@ -80,6 +78,60 @@ class ProctoredExam(TimeStampedModel):
return
cls
.
objects
.
filter
(
course_id
=
course_id
)
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 student has completed the exam
ready_to_submit
=
'ready_to_submit'
#
# The follow statuses below are considered in a 'completed' state
# and we will not allow transitions to status above this mark
#
# the student declined to take the exam as a proctored exam
declined
=
'declined'
# the exam has timed out
timed_out
=
'timed_out'
# 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 was not reviewed
not_reviewed
=
'not_reviewed'
# the exam is believed to be in error
error
=
'error'
class
ProctoredExamStudentAttemptManager
(
models
.
Manager
):
"""
Custom manager
...
...
@@ -137,62 +189,13 @@ class ProctoredExamStudentAttemptManager(models.Manager):
"""
Returns the active student exams (user in-progress exams)
"""
filtered_query
=
Q
(
user_id
=
user_id
)
&
Q
(
sta
rted_at__isnull
=
False
)
&
Q
(
completed_at__isnull
=
True
)
filtered_query
=
Q
(
user_id
=
user_id
)
&
Q
(
sta
tus
=
ProctoredExamStudentAttemptStatus
.
started
)
if
course_id
is
not
None
:
filtered_query
=
filtered_query
&
Q
(
proctored_exam__course_id
=
course_id
)
return
self
.
filter
(
filtered_query
)
.
order_by
(
'-created'
)
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 student declined to take the exam as a proctored exam
declined
=
'declined'
# 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 was not reviewed
not_reviewed
=
'not_reviewed'
# the exam is believed to be in error
error
=
'error'
class
ProctoredExamStudentAttempt
(
TimeStampedModel
):
"""
Information about the Student Attempt on a
...
...
@@ -206,6 +209,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# started/completed date times
started_at
=
models
.
DateTimeField
(
null
=
True
)
# completed_at means when the attempt was 'submitted'
completed_at
=
models
.
DateTimeField
(
null
=
True
)
last_poll_timestamp
=
models
.
DateTimeField
(
null
=
True
)
...
...
@@ -240,11 +245,6 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
verbose_name
=
'proctored exam attempt'
unique_together
=
((
'user'
,
'proctored_exam'
),)
@property
def
is_active
(
self
):
""" returns boolean if this attempt is considered active """
return
self
.
started_at
and
not
self
.
completed_at
@classmethod
def
create_exam_attempt
(
cls
,
exam_id
,
user_id
,
student_name
,
allowed_time_limit_mins
,
attempt_code
,
taking_as_proctored
,
is_sample_attempt
,
external_id
):
...
...
@@ -265,14 +265,6 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
status
=
ProctoredExamStudentAttemptStatus
.
created
,
)
def
start_exam_attempt
(
self
):
"""
sets the model's state when an exam attempt has started
"""
self
.
started_at
=
datetime
.
now
(
pytz
.
UTC
)
self
.
status
=
ProctoredExamStudentAttemptStatus
.
started
self
.
save
()
def
delete_exam_attempt
(
self
):
"""
deletes the exam attempt object and archives it to the ProctoredExamStudentAttemptHistory table.
...
...
edx_proctoring/templates/proctoring/proctoring_launch_callback.html
View file @
a8ba1176
...
...
@@ -213,8 +213,8 @@
var
url
=
'{{exam_attempt_status_url}}'
;
$
.
ajax
(
url
).
success
(
function
(
data
){
// has the end user completed the exam in the LMS?!?
if
(
data
.
status
===
'
completed'
||
data
.
status
===
'submitted'
||
data
.
status
===
'verifi
ed'
)
{
// has the end user completed
and submitted
the exam in the LMS?!?
if
(
data
.
status
===
'
submitt
ed'
)
{
// Signal that the desktop software should terminate
// NOTE: This is per the API documentation from SoftwareSecure
window
.
external
.
quitApplication
();
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_
completed
.html
→
edx_proctoring/templates/proctoring/seq_proctored_exam_
ready_to_submit
.html
View file @
a8ba1176
...
...
@@ -11,11 +11,22 @@
Your worked will then be graded and your proctored session will be reviewed separately.
{% endblocktrans %}
</p>
<button
type=
"button"
name=
"submit-proctored-exam"
>
{% blocktrans %}
I'm ready! Submit my answers and end my proctored exam
{% endblocktrans %}
</button>
<div>
<button
type=
"button"
name=
"submit-proctored-exam"
class=
"exam-action-button"
data-action=
"submit"
data-exam-id=
"{{exam_id}}"
data-change-state-url=
"{{change_state_url}}"
>
{% blocktrans %}
I'm ready! Submit my answers and end my proctored exam.
{% endblocktrans %}
</button>
</div>
{% if does_time_remain %}
<div>
<button
type=
"button"
name=
"goback-proctored-exam"
class=
"exam-action-button"
data-action=
"start"
data-exam-id=
"{{exam_id}}"
data-change-state-url=
"{{change_state_url}}"
>
{% blocktrans %}
No, I am not ready! I'd like to continue my work.
{% endblocktrans %}
</button>
</div>
{% endif %}
</div>
<div
class=
"footer-sequence border-b-0 padding-b-0"
>
<span>
{% trans "What happens next ?" %}
</span>
...
...
@@ -27,3 +38,25 @@
{% endblocktrans %}
</p>
</div>
<script
type=
"text/javascript"
>
$
(
'.exam-action-button'
).
click
(
function
(
event
)
{
var
action_url
=
$
(
this
).
data
(
'change-state-url'
);
var
exam_id
=
$
(
this
).
data
(
'exam-id'
);
var
action
=
$
(
this
).
data
(
'action'
)
// Update the state of the attempt
$
.
ajax
({
url
:
action_url
,
type
:
'PUT'
,
data
:
{
action
:
action
},
success
:
function
()
{
// Reloading page will reflect the new state of the attempt
location
.
reload
()
}
});
}
);
</script>
edx_proctoring/templates/proctoring/seq_timed_exam_
completed
.html
→
edx_proctoring/templates/proctoring/seq_timed_exam_
ready_to_submit
.html
View file @
a8ba1176
File moved
edx_proctoring/tests/test_api.py
View file @
a8ba1176
...
...
@@ -3,6 +3,7 @@
"""
All tests for the models.py
"""
import
ddt
from
datetime
import
datetime
,
timedelta
from
mock
import
patch
import
pytz
...
...
@@ -39,7 +40,8 @@ from edx_proctoring.exceptions import (
StudentExamAttemptAlreadyExistsException
,
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptedAlreadyStarted
,
UserNotFoundException
UserNotFoundException
,
ProctoredExamIllegalStatusTransition
)
from
edx_proctoring.models
import
(
ProctoredExam
,
...
...
@@ -56,6 +58,7 @@ from edx_proctoring.tests.test_services import MockCreditService
from
edx_proctoring.runtime
import
set_runtime_service
,
get_runtime_service
@ddt.ddt
class
ProctoredExamApiTests
(
LoggedInTestCase
):
"""
All tests for the models.py
...
...
@@ -497,8 +500,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
Test to get the all the active
exams for the user.
"""
active_exam_attempt
=
self
.
_create_started_exam_attempt
()
self
.
assertEqual
(
active_exam_attempt
.
is_active
,
True
)
self
.
_create_started_exam_attempt
()
exam_id
=
create_exam
(
course_id
=
self
.
course_id
,
content_id
=
'test_content_2'
,
...
...
@@ -938,7 +940,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
Test for get_student_view proctored exam which has been completed.
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
exam_attempt
.
status
=
"completed"
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
ready_to_submit
exam_attempt
.
save
()
rendered_response
=
get_student_view
(
...
...
@@ -1022,7 +1024,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
Test for get_student_view timed exam which has completed.
"""
exam_attempt
=
self
.
_create_started_exam_attempt
(
is_proctored
=
False
)
exam_attempt
.
status
=
"completed"
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
ready_to_submit
exam_attempt
.
save
()
rendered_response
=
get_student_view
(
...
...
@@ -1057,3 +1059,55 @@ class ProctoredExamApiTests(LoggedInTestCase):
credit_status
[
'credit_requirement_status'
][
0
][
'status'
],
'submitted'
)
def
test_error_credit_state
(
self
):
"""
Verify that putting an attempt into the error state will also mark
the credit requirement as failed
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
ProctoredExamStudentAttemptStatus
.
error
)
credit_service
=
get_runtime_service
(
'credit'
)
credit_status
=
credit_service
.
get_credit_state
(
self
.
user
.
id
,
exam_attempt
.
proctored_exam
.
course_id
)
self
.
assertEqual
(
len
(
credit_status
[
'credit_requirement_status'
]),
1
)
self
.
assertEqual
(
credit_status
[
'credit_requirement_status'
][
0
][
'status'
],
'failed'
)
@ddt.data
(
(
ProctoredExamStudentAttemptStatus
.
declined
,
ProctoredExamStudentAttemptStatus
.
eligible
),
(
ProctoredExamStudentAttemptStatus
.
timed_out
,
ProctoredExamStudentAttemptStatus
.
created
),
(
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
ready_to_start
),
(
ProctoredExamStudentAttemptStatus
.
verified
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
error
,
ProctoredExamStudentAttemptStatus
.
started
),
)
@ddt.unpack
def
test_illegal_status_transition
(
self
,
from_status
,
to_status
):
"""
Verify that we cannot reset backwards an attempt status
once it is in a completed state
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
from_status
)
with
self
.
assertRaises
(
ProctoredExamIllegalStatusTransition
):
print
'*** from = {} to {}'
.
format
(
from_status
,
to_status
)
update_attempt_status
(
exam_attempt
.
proctored_exam_id
,
self
.
user
.
id
,
to_status
)
edx_proctoring/tests/test_views.py
View file @
a8ba1176
...
...
@@ -13,7 +13,12 @@ from django.test.client import Client
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
django.contrib.auth.models
import
User
from
edx_proctoring.models
import
ProctoredExam
,
ProctoredExamStudentAttempt
,
ProctoredExamStudentAllowance
from
edx_proctoring.models
import
(
ProctoredExam
,
ProctoredExamStudentAttempt
,
ProctoredExamStudentAllowance
,
ProctoredExamStudentAttemptStatus
,
)
from
edx_proctoring.views
import
require_staff
from
edx_proctoring.api
import
(
create_exam
,
...
...
@@ -706,6 +711,67 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'exam_attempt_id'
],
old_attempt_id
)
def
test_submit_exam_attempt
(
self
):
"""
Tries to submit an exam
"""
# 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
,
'user_id'
:
self
.
student_taking_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
}
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'
]
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
old_attempt_id
]),
{
'action'
:
'submit'
,
}
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'exam_attempt_id'
],
old_attempt_id
)
attempt
=
get_exam_attempt_by_id
(
response_data
[
'exam_attempt_id'
])
self
.
assertEqual
(
attempt
[
'status'
],
ProctoredExamStudentAttemptStatus
.
submitted
)
# we should not be able to restart it
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
old_attempt_id
]),
{
'action'
:
'start'
,
}
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
old_attempt_id
]),
{
'action'
:
'stop'
,
}
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_get_exam_attempts
(
self
):
"""
Test to get the exam attempts in a course.
...
...
edx_proctoring/views.py
View file @
a8ba1176
...
...
@@ -30,7 +30,6 @@ from edx_proctoring.api import (
get_all_exam_attempts
,
remove_exam_attempt
,
get_filtered_exam_attempts
,
update_exam_attempt
,
update_attempt_status
)
from
edx_proctoring.exceptions
import
(
...
...
@@ -283,7 +282,11 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
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'
)
update_attempt_status
(
attempt
[
'proctored_exam'
][
'id'
],
attempt
[
'user'
][
'id'
],
ProctoredExamStudentAttemptStatus
.
error
)
return
Response
(
data
=
attempt
,
...
...
@@ -334,9 +337,16 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
exam_id
=
attempt
[
'proctored_exam'
][
'id'
],
user_id
=
request
.
user
.
id
)
elif
action
==
'submit'
:
exam_attempt_id
=
update_attempt_status
(
attempt
[
'proctored_exam'
][
'id'
],
request
.
user
.
id
,
ProctoredExamStudentAttemptStatus
.
submitted
)
return
Response
({
"exam_attempt_id"
:
exam_attempt_id
})
except
ProctoredBaseException
,
ex
:
LOG
.
exception
(
ex
)
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
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