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
40f36c45
Commit
40f36c45
authored
Jul 16, 2015
by
Chris Dodge
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
timer shouldn't start until the user acknowledges an alert in our window
parent
57320a34
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
147 additions
and
32 deletions
+147
-32
edx_proctoring/api.py
+29
-2
edx_proctoring/callbacks.py
+6
-7
edx_proctoring/models.py
+10
-10
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
+1
-0
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
+5
-2
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
+16
-5
edx_proctoring/tests/test_api.py
+16
-0
edx_proctoring/tests/test_views.py
+51
-2
edx_proctoring/views.py
+13
-4
No files found.
edx_proctoring/api.py
View file @
40f36c45
...
@@ -183,6 +183,17 @@ def get_exam_attempt_by_id(attempt_id):
...
@@ -183,6 +183,17 @@ def get_exam_attempt_by_id(attempt_id):
return
serialized_attempt_obj
.
data
if
exam_attempt_obj
else
None
return
serialized_attempt_obj
.
data
if
exam_attempt_obj
else
None
def
get_exam_attempt_by_code
(
attempt_code
):
"""
Signals the beginning of an exam attempt when we only have
an attempt code
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt_by_code
(
attempt_code
)
serialized_attempt_obj
=
ProctoredExamStudentAttemptSerializer
(
exam_attempt_obj
)
return
serialized_attempt_obj
.
data
if
exam_attempt_obj
else
None
def
create_exam_attempt
(
exam_id
,
user_id
,
taking_as_proctored
=
False
):
def
create_exam_attempt
(
exam_id
,
user_id
,
taking_as_proctored
=
False
):
"""
"""
Creates an exam attempt for user_id against exam_id. There should only be
Creates an exam attempt for user_id against exam_id. There should only be
...
@@ -264,7 +275,7 @@ def start_exam_attempt(exam_id, user_id):
...
@@ -264,7 +275,7 @@ def start_exam_attempt(exam_id, user_id):
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
_start_exam_attempt
(
existing_attempt
)
return
_start_exam_attempt
(
existing_attempt
)
def
start_exam_attempt_by_code
(
attempt_code
):
def
start_exam_attempt_by_code
(
attempt_code
):
...
@@ -283,7 +294,7 @@ def start_exam_attempt_by_code(attempt_code):
...
@@ -283,7 +294,7 @@ def start_exam_attempt_by_code(attempt_code):
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
_start_exam_attempt
(
existing_attempt
)
return
_start_exam_attempt
(
existing_attempt
)
def
_start_exam_attempt
(
existing_attempt
):
def
_start_exam_attempt
(
existing_attempt
):
...
@@ -302,6 +313,8 @@ def _start_exam_attempt(existing_attempt):
...
@@ -302,6 +313,8 @@ def _start_exam_attempt(existing_attempt):
existing_attempt
.
start_exam_attempt
()
existing_attempt
.
start_exam_attempt
()
return
existing_attempt
.
id
def
stop_exam_attempt
(
exam_id
,
user_id
):
def
stop_exam_attempt
(
exam_id
,
user_id
):
"""
"""
...
@@ -330,6 +343,20 @@ def mark_exam_attempt_timeout(exam_id, user_id):
...
@@ -330,6 +343,20 @@ def mark_exam_attempt_timeout(exam_id, user_id):
return
exam_attempt_obj
.
id
return
exam_attempt_obj
.
id
def
mark_exam_attempt_as_ready
(
exam_id
,
user_id
):
"""
Marks the exam attemp as ready to start
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt
(
exam_id
,
user_id
)
if
exam_attempt_obj
is
None
:
raise
StudentExamAttemptDoesNotExistsException
(
'Error. Trying to time out an exam that does not exist.'
)
else
:
exam_attempt_obj
.
status
=
ProctoredExamStudentAttemptStatus
.
ready_to_start
exam_attempt_obj
.
save
()
return
exam_attempt_obj
.
id
def
remove_exam_attempt_by_id
(
attempt_id
):
def
remove_exam_attempt_by_id
(
attempt_id
):
"""
"""
Removes an exam attempt given the attempt id.
Removes an exam attempt given the attempt id.
...
...
edx_proctoring/callbacks.py
View file @
40f36c45
...
@@ -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
({})))
edx_proctoring/models.py
View file @
40f36c45
...
@@ -151,37 +151,37 @@ class ProctoredExamStudentAttemptStatus(object):
...
@@ -151,37 +151,37 @@ class ProctoredExamStudentAttemptStatus(object):
"""
"""
# the student is eligible to decide if he/she wants to persue credit
# the student is eligible to decide if he/she wants to persue credit
eligible
=
'
E
ligible'
eligible
=
'
e
ligible'
# the attempt record has been created, but the exam has not yet
# the attempt record has been created, but the exam has not yet
# been started
# been started
created
=
'
C
reated'
created
=
'
c
reated'
# the attempt is ready to start but requires
# the attempt is ready to start but requires
# user to acknowledge that he/she wants to start the exam
# user to acknowledge that he/she wants to start the exam
ready_to_start
=
'
Ready to
start'
ready_to_start
=
'
ready_to_
start'
# the student has started the exam and is
# the student has started the exam and is
# in the process of completing the exam
# in the process of completing the exam
started
=
'
S
tarted'
started
=
'
s
tarted'
# the exam has timed out
# the exam has timed out
timed_out
=
'
Timed O
ut'
timed_out
=
'
timed_o
ut'
# the student has completed the exam
# the student has completed the exam
completed
=
'
C
ompleted'
completed
=
'
c
ompleted'
# the student has submitted the exam for proctoring review
# the student has submitted the exam for proctoring review
submitted
=
'
S
ubmitted'
submitted
=
'
s
ubmitted'
# the exam has been verified and approved
# the exam has been verified and approved
verified
=
'
V
erified'
verified
=
'
v
erified'
# the exam has been rejected
# the exam has been rejected
rejected
=
'
R
ejected'
rejected
=
'
r
ejected'
# the exam is believed to be in error
# the exam is believed to be in error
error
=
'
E
rror'
error
=
'
e
rror'
class
ProctoredExamStudentAttempt
(
TimeStampedModel
):
class
ProctoredExamStudentAttempt
(
TimeStampedModel
):
...
...
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
View file @
40f36c45
...
@@ -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
()
{
...
...
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
View file @
40f36c45
...
@@ -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
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
View file @
40f36c45
...
@@ -96,17 +96,28 @@
...
@@ -96,17 +96,28 @@
function
poll_exam_started
()
{
function
poll_exam_started
()
{
var
url
=
$
(
'.instructions'
).
data
(
'exam-started-poll-url'
)
var
url
=
$
(
'.instructions'
).
data
(
'exam-started-poll-url'
)
$
.
ajax
(
url
).
success
(
function
(
data
){
$
.
ajax
(
url
).
success
(
function
(
data
){
if
(
data
.
sta
rted_at
!==
null
)
{
if
(
data
.
sta
tus
===
'ready_to_start'
)
{
if
(
_waiting_for_proctored_interval
!=
null
)
{
if
(
_waiting_for_proctored_interval
!=
null
)
{
clearInterval
(
_waiting_for_proctored_interval
)
clearInterval
(
_waiting_for_proctored_interval
)
}
}
// Let the student know exam
has started and clock is running
.
// Let the student know exam
is ready to start
.
//
this
may or may not bring the browser window back to the
//
This alert
may or may not bring the browser window back to the
// foreground (depending on browser as well as user settings)
// foreground (depending on browser as well as user settings)
alert
(
'{% trans "Your proctored exam has started, please click OK to enter into your exam." %}'
)
alert
(
'{% trans "Your proctored exam has started, please click OK to enter into your exam." %}'
)
// Reloading page will reflect the new state of the attempt
// after the user acknowledges the alert then we can start
location
.
reload
()
// the exam and timer
$
.
ajax
({
url
:
url
,
type
:
'PUT'
,
data
:
{
action
:
'start'
},
success
:
function
()
{
// Reloading page will reflect the new state of the attempt
location
.
reload
()
}
});
}
}
});
});
}
}
...
...
edx_proctoring/tests/test_api.py
View file @
40f36c45
...
@@ -28,6 +28,7 @@ from edx_proctoring.api import (
...
@@ -28,6 +28,7 @@ from edx_proctoring.api import (
get_filtered_exam_attempts
,
get_filtered_exam_attempts
,
is_feature_enabled
,
is_feature_enabled
,
mark_exam_attempt_timeout
,
mark_exam_attempt_timeout
,
mark_exam_attempt_as_ready
,
)
)
from
edx_proctoring.exceptions
import
(
from
edx_proctoring.exceptions
import
(
ProctoredExamAlreadyExists
,
ProctoredExamAlreadyExists
,
...
@@ -389,6 +390,21 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -389,6 +390,21 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
)
self
.
assertEqual
(
proctored_exam_student_attempt
.
id
,
proctored_exam_attempt_id
)
self
.
assertEqual
(
proctored_exam_student_attempt
.
id
,
proctored_exam_attempt_id
)
def
test_mark_exam_attempt_as_ready
(
self
):
"""
Tests the mark exam as timed out
"""
with
self
.
assertRaises
(
StudentExamAttemptDoesNotExistsException
):
mark_exam_attempt_as_ready
(
self
.
proctored_exam_id
,
self
.
user_id
)
proctored_exam_student_attempt
=
self
.
_create_unstarted_exam_attempt
()
self
.
assertIsNone
(
proctored_exam_student_attempt
.
completed_at
)
proctored_exam_attempt_id
=
mark_exam_attempt_as_ready
(
proctored_exam_student_attempt
.
proctored_exam
,
self
.
user_id
)
self
.
assertEqual
(
proctored_exam_student_attempt
.
id
,
proctored_exam_attempt_id
)
def
test_get_active_exams_for_user
(
self
):
def
test_get_active_exams_for_user
(
self
):
"""
"""
Test to get the all the active
Test to get the all the active
...
...
edx_proctoring/tests/test_views.py
View file @
40f36c45
...
@@ -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
.
assert
IsNotNone
(
attempt
[
'started_at'
]
)
self
.
assert
Equal
(
attempt
[
'status'
],
'ready_to_start'
)
def
test_bad_exam_code_callback
(
self
):
def
test_bad_exam_code_callback
(
self
):
"""
"""
...
...
edx_proctoring/views.py
View file @
40f36c45
...
@@ -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
=
{
...
...
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