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
6e7b4dba
Commit
6e7b4dba
authored
Jul 16, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #29 from edx/cdodge/punchlist2
Add status to attempts
parents
d7d6afe8
40f36c45
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
241 additions
and
38 deletions
+241
-38
edx_proctoring/api.py
+51
-6
edx_proctoring/callbacks.py
+6
-7
edx_proctoring/models.py
+46
-1
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
+1
-0
edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js
+8
-3
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
+9
-6
edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore
+2
-3
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
+22
-6
edx_proctoring/tests/test_api.py
+32
-0
edx_proctoring/tests/test_views.py
+51
-2
edx_proctoring/views.py
+13
-4
No files found.
edx_proctoring/api.py
View file @
6e7b4dba
...
@@ -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
)
...
...
edx_proctoring/callbacks.py
View file @
6e7b4dba
...
@@ -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 @
6e7b4dba
...
@@ -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
):
...
...
edx_proctoring/static/proctoring/js/models/proctored_exam_model.js
View file @
6e7b4dba
...
@@ -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_attempt_view.js
View file @
6e7b4dba
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
);
edx_proctoring/static/proctoring/js/views/proctored_exam_view.js
View file @
6e7b4dba
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
);
edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore
View file @
6e7b4dba
...
@@ -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.do
e
@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
edx_proctoring/templates/proctoring/seq_proctored_exam_instructions.html
View file @
6e7b4dba
...
@@ -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
()
}
});
}
}
});
});
}
}
...
...
edx_proctoring/tests/test_api.py
View file @
6e7b4dba
...
@@ -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
...
...
edx_proctoring/tests/test_views.py
View file @
6e7b4dba
...
@@ -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 @
6e7b4dba
...
@@ -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