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
...
@@ -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_
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
:
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
:
...
...
edx_proctoring/exceptions.py
View file @
17ef189f
...
@@ -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
(
ProctoredBase
Exception
):
"""
"""
Raised when a look up fails.
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.
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.
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):
...
@@ -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_
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)
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 @@
...
@@ -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
);
...
...
edx_proctoring/static/proctoring/js/proctored_exam_view.js
View file @
17ef189f
...
@@ -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
();
...
...
edx_proctoring/templates/proctoring/seq_proctored_exam_entrance.html
View file @
17ef189f
...
@@ -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
...
...
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 @@
...
@@ -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
...
...
edx_proctoring/tests/test_api.py
View file @
17ef189f
...
@@ -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
)
...
...
edx_proctoring/tests/test_views.py
View file @
17ef189f
...
@@ -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
):
"""
"""
...
...
edx_proctoring/views.py
View file @
17ef189f
...
@@ -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
)
}
)
)
...
...
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