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
34cd882f
Commit
34cd882f
authored
Jul 16, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #23 from edx/muhhshoaib/PHX-11
(WIP) PHX-11
parents
edc4d2a1
19ceb59d
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
784 additions
and
61 deletions
+784
-61
edx_proctoring/api.py
+41
-7
edx_proctoring/migrations/0003_auto__add_proctoredexamstudentattempthistory.py
+0
-0
edx_proctoring/models.py
+127
-39
edx_proctoring/serializers.py
+4
-4
edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_collection.js
+14
-0
edx_proctoring/static/proctoring/js/models/proctored_exam_attempt_model.js
+15
-0
edx_proctoring/static/proctoring/js/proctored_app.js
+6
-0
edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js
+151
-0
edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore
+122
-0
edx_proctoring/tests/test_api.py
+60
-4
edx_proctoring/tests/test_models.py
+39
-1
edx_proctoring/tests/test_views.py
+124
-1
edx_proctoring/urls.py
+11
-0
edx_proctoring/views.py
+70
-5
No files found.
edx_proctoring/api.py
View file @
34cd882f
...
@@ -160,7 +160,7 @@ def get_exam_attempt(exam_id, user_id):
...
@@ -160,7 +160,7 @@ 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_exam_attempt
(
exam_id
,
user_id
)
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt
(
exam_id
,
user_id
)
serialized_attempt_obj
=
ProctoredExamStudentAttemptSerializer
(
exam_attempt_obj
)
serialized_attempt_obj
=
ProctoredExamStudentAttemptSerializer
(
exam_attempt_obj
)
return
serialized_attempt_obj
.
data
if
exam_attempt_obj
else
None
return
serialized_attempt_obj
.
data
if
exam_attempt_obj
else
None
...
@@ -169,7 +169,7 @@ def get_exam_attempt_by_id(attempt_id):
...
@@ -169,7 +169,7 @@ def get_exam_attempt_by_id(attempt_id):
"""
"""
Return an existing exam attempt for the given student
Return an existing exam attempt for the given student
"""
"""
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
get_exam_attempt_by_id
(
attempt_id
)
exam_attempt_obj
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt_by_id
(
attempt_id
)
serialized_attempt_obj
=
ProctoredExamStudentAttemptSerializer
(
exam_attempt_obj
)
serialized_attempt_obj
=
ProctoredExamStudentAttemptSerializer
(
exam_attempt_obj
)
return
serialized_attempt_obj
.
data
if
exam_attempt_obj
else
None
return
serialized_attempt_obj
.
data
if
exam_attempt_obj
else
None
...
@@ -180,7 +180,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
...
@@ -180,7 +180,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
one exam_attempt per user per exam. Multiple attempts by user will be archived
one exam_attempt per user per exam. Multiple attempts by user will be archived
in a separate table
in a separate table
"""
"""
if
ProctoredExamStudentAttempt
.
get_exam_attempt
(
exam_id
,
user_id
):
if
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt
(
exam_id
,
user_id
):
err_msg
=
(
err_msg
=
(
'Cannot create new exam attempt for exam_id = {exam_id} and '
'Cannot create new exam attempt for exam_id = {exam_id} and '
'user_id = {user_id} because it already exists!'
'user_id = {user_id} because it already exists!'
...
@@ -245,7 +245,7 @@ def start_exam_attempt(exam_id, user_id):
...
@@ -245,7 +245,7 @@ def start_exam_attempt(exam_id, user_id):
Returns: exam_attempt_id (PK)
Returns: exam_attempt_id (PK)
"""
"""
existing_attempt
=
ProctoredExamStudentAttempt
.
get_exam_attempt
(
exam_id
,
user_id
)
existing_attempt
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt
(
exam_id
,
user_id
)
if
not
existing_attempt
:
if
not
existing_attempt
:
err_msg
=
(
err_msg
=
(
...
@@ -264,7 +264,7 @@ def start_exam_attempt_by_code(attempt_code):
...
@@ -264,7 +264,7 @@ def start_exam_attempt_by_code(attempt_code):
an attempt code
an attempt code
"""
"""
existing_attempt
=
ProctoredExamStudentAttempt
.
get_exam_attempt_by_code
(
attempt_code
)
existing_attempt
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt_by_code
(
attempt_code
)
if
not
existing_attempt
:
if
not
existing_attempt
:
err_msg
=
(
err_msg
=
(
...
@@ -298,7 +298,7 @@ def stop_exam_attempt(exam_id, user_id):
...
@@ -298,7 +298,7 @@ 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_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 is not in progress.'
)
else
:
else
:
...
@@ -307,6 +307,24 @@ def stop_exam_attempt(exam_id, user_id):
...
@@ -307,6 +307,24 @@ def stop_exam_attempt(exam_id, user_id):
return
exam_attempt_obj
.
id
return
exam_attempt_obj
.
id
def
remove_exam_attempt_by_id
(
attempt_id
):
"""
Removes an exam attempt given the attempt id.
"""
existing_attempt
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt_by_id
(
attempt_id
)
if
not
existing_attempt
:
err_msg
=
(
'Cannot remove attempt for attempt_id = {attempt_id} '
'because it does not exist!'
)
.
format
(
attempt_id
=
attempt_id
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
existing_attempt
.
delete_exam_attempt
()
def
get_all_exams_for_course
(
course_id
):
def
get_all_exams_for_course
(
course_id
):
"""
"""
This method will return all exams for a course. This will return a list
This method will return all exams for a course. This will return a list
...
@@ -336,6 +354,22 @@ def get_all_exams_for_course(course_id):
...
@@ -336,6 +354,22 @@ def get_all_exams_for_course(course_id):
return
[
ProctoredExamSerializer
(
proctored_exam
)
.
data
for
proctored_exam
in
exams
]
return
[
ProctoredExamSerializer
(
proctored_exam
)
.
data
for
proctored_exam
in
exams
]
def
get_all_exam_attempts
(
course_id
):
"""
Returns all the exam attempts for the course id.
"""
exam_attempts
=
ProctoredExamStudentAttempt
.
objects
.
get_all_exam_attempts
(
course_id
)
return
[
ProctoredExamStudentAttemptSerializer
(
active_exam
)
.
data
for
active_exam
in
exam_attempts
]
def
get_filtered_exam_attempts
(
course_id
,
search_by
):
"""
returns all exam attempts for a course id filtered by the search_by string in user names and emails.
"""
exam_attempts
=
ProctoredExamStudentAttempt
.
objects
.
get_filtered_exam_attempts
(
course_id
,
search_by
)
return
[
ProctoredExamStudentAttemptSerializer
(
active_exam
)
.
data
for
active_exam
in
exam_attempts
]
def
get_active_exams_for_user
(
user_id
,
course_id
=
None
):
def
get_active_exams_for_user
(
user_id
,
course_id
=
None
):
"""
"""
This method will return a list of active exams for the user,
This method will return a list of active exams for the user,
...
@@ -357,7 +391,7 @@ def get_active_exams_for_user(user_id, course_id=None):
...
@@ -357,7 +391,7 @@ def get_active_exams_for_user(user_id, course_id=None):
"""
"""
result
=
[]
result
=
[]
student_active_exams
=
ProctoredExamStudentAttempt
.
get_active_student_attempts
(
user_id
,
course_id
)
student_active_exams
=
ProctoredExamStudentAttempt
.
objects
.
get_active_student_attempts
(
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.
...
...
edx_proctoring/migrations/0003_auto__add_proctoredexamstudentattempthistory.py
0 → 100644
View file @
34cd882f
This diff is collapsed.
Click to expand it.
edx_proctoring/models.py
View file @
34cd882f
...
@@ -11,6 +11,7 @@ from model_utils.models import TimeStampedModel
...
@@ -11,6 +11,7 @@ from model_utils.models import TimeStampedModel
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
edx_proctoring.exceptions
import
UserNotFoundException
from
edx_proctoring.exceptions
import
UserNotFoundException
from
django.db.models.base
import
ObjectDoesNotExist
class
ProctoredExam
(
TimeStampedModel
):
class
ProctoredExam
(
TimeStampedModel
):
...
@@ -76,11 +77,77 @@ class ProctoredExam(TimeStampedModel):
...
@@ -76,11 +77,77 @@ class ProctoredExam(TimeStampedModel):
return
cls
.
objects
.
filter
(
course_id
=
course_id
)
return
cls
.
objects
.
filter
(
course_id
=
course_id
)
class
ProctoredExamStudentAttemptManager
(
models
.
Manager
):
"""
Custom manager
"""
def
get_exam_attempt
(
self
,
exam_id
,
user_id
):
"""
Returns the Student Exam Attempt object if found
else Returns None.
"""
try
:
exam_attempt_obj
=
self
.
get
(
proctored_exam_id
=
exam_id
,
user_id
=
user_id
)
except
ObjectDoesNotExist
:
# pylint: disable=no-member
exam_attempt_obj
=
None
return
exam_attempt_obj
def
get_exam_attempt_by_id
(
self
,
attempt_id
):
"""
Returns the Student Exam Attempt by the attempt_id else return None
"""
try
:
exam_attempt_obj
=
self
.
get
(
id
=
attempt_id
)
except
ObjectDoesNotExist
:
# pylint: disable=no-member
exam_attempt_obj
=
None
return
exam_attempt_obj
def
get_exam_attempt_by_code
(
self
,
attempt_code
):
"""
Returns the Student Exam Attempt object if found
else Returns None.
"""
try
:
exam_attempt_obj
=
self
.
get
(
attempt_code
=
attempt_code
)
except
ObjectDoesNotExist
:
# pylint: disable=no-member
exam_attempt_obj
=
None
return
exam_attempt_obj
def
get_all_exam_attempts
(
self
,
course_id
):
"""
Returns the Student Exam Attempts for the given course_id.
"""
return
self
.
filter
(
proctored_exam__course_id
=
course_id
)
def
get_filtered_exam_attempts
(
self
,
course_id
,
search_by
):
"""
Returns the Student Exam Attempts for the given course_id filtered by search_by.
"""
filtered_query
=
Q
(
proctored_exam__course_id
=
course_id
)
&
(
Q
(
user__username__contains
=
search_by
)
|
Q
(
user__email__contains
=
search_by
)
)
return
self
.
filter
(
filtered_query
)
def
get_active_student_attempts
(
self
,
user_id
,
course_id
=
None
):
"""
Returns the active student exams (user in-progress exams)
"""
filtered_query
=
Q
(
user_id
=
user_id
)
&
Q
(
started_at__isnull
=
False
)
&
Q
(
completed_at__isnull
=
True
)
if
course_id
is
not
None
:
filtered_query
=
filtered_query
&
Q
(
proctored_exam__course_id
=
course_id
)
return
self
.
filter
(
filtered_query
)
class
ProctoredExamStudentAttempt
(
TimeStampedModel
):
class
ProctoredExamStudentAttempt
(
TimeStampedModel
):
"""
"""
Information about the Student Attempt on a
Information about the Student Attempt on a
Proctored Exam.
Proctored Exam.
"""
"""
objects
=
ProctoredExamStudentAttemptManager
()
user
=
models
.
ForeignKey
(
User
,
db_index
=
True
)
user
=
models
.
ForeignKey
(
User
,
db_index
=
True
)
proctored_exam
=
models
.
ForeignKey
(
ProctoredExam
,
db_index
=
True
)
proctored_exam
=
models
.
ForeignKey
(
ProctoredExam
,
db_index
=
True
)
...
@@ -148,51 +215,72 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
...
@@ -148,51 +215,72 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
self
.
started_at
=
datetime
.
now
(
pytz
.
UTC
)
self
.
started_at
=
datetime
.
now
(
pytz
.
UTC
)
self
.
save
()
self
.
save
()
@classmethod
def
delete_exam_attempt
(
self
):
def
get_exam_attempt
(
cls
,
exam_id
,
user_id
):
"""
"""
Returns the Student Exam Attempt object if found
deletes the exam attempt object.
else Returns None.
"""
"""
try
:
self
.
delete
()
exam_attempt_obj
=
cls
.
objects
.
get
(
proctored_exam_id
=
exam_id
,
user_id
=
user_id
)
except
cls
.
DoesNotExist
:
# pylint: disable=no-member
exam_attempt_obj
=
None
return
exam_attempt_obj
@classmethod
def
get_exam_attempt_by_id
(
cls
,
attempt_id
):
"""
Returns the Student Exam Attempt by the attempt_id else return None
"""
try
:
exam_attempt_obj
=
cls
.
objects
.
get
(
id
=
attempt_id
)
except
cls
.
DoesNotExist
:
# pylint: disable=no-member
exam_attempt_obj
=
None
return
exam_attempt_obj
@classmethod
class
ProctoredExamStudentAttemptHistory
(
TimeStampedModel
):
def
get_exam_attempt_by_code
(
cls
,
attempt_code
):
"""
"""
This should be the same schema as ProctoredExamStudentAttempt
Returns the Student Exam Attempt object if found
but will record (for audit history) all entries that have been updated.
else Returns None.
"""
"""
try
:
exam_attempt_obj
=
cls
.
objects
.
get
(
attempt_code
=
attempt_code
)
except
cls
.
DoesNotExist
:
# pylint: disable=no-member
exam_attempt_obj
=
None
return
exam_attempt_obj
@classmethod
user
=
models
.
ForeignKey
(
User
,
db_index
=
True
)
def
get_active_student_attempts
(
cls
,
user_id
,
course_id
=
None
):
"""
proctored_exam
=
models
.
ForeignKey
(
ProctoredExam
,
db_index
=
True
)
Returns the active student exams (user in-progress exams)
"""
filtered_query
=
Q
(
user_id
=
user_id
)
&
Q
(
started_at__isnull
=
False
)
&
Q
(
completed_at__isnull
=
True
)
if
course_id
is
not
None
:
filtered_query
=
filtered_query
&
Q
(
proctored_exam__course_id
=
course_id
)
return
cls
.
objects
.
filter
(
filtered_query
)
# started/completed date times
started_at
=
models
.
DateTimeField
(
null
=
True
)
completed_at
=
models
.
DateTimeField
(
null
=
True
)
# this will be a unique string ID that the user
# will have to use when starting the proctored exam
attempt_code
=
models
.
CharField
(
max_length
=
255
,
null
=
True
,
db_index
=
True
)
# This will be a integration specific ID - say to SoftwareSecure.
external_id
=
models
.
CharField
(
max_length
=
255
,
null
=
True
,
db_index
=
True
)
# this is the time limit allowed to the student
allowed_time_limit_mins
=
models
.
IntegerField
()
# what is the status of this attempt
status
=
models
.
CharField
(
max_length
=
64
)
# if the user is attempting this as a proctored exam
# in case there is an option to opt-out
taking_as_proctored
=
models
.
BooleanField
()
# Whether this attampt is considered a sample attempt, e.g. to try out
# the proctoring software
is_sample_attempt
=
models
.
BooleanField
()
student_name
=
models
.
CharField
(
max_length
=
255
)
@receiver
(
pre_delete
,
sender
=
ProctoredExamStudentAttempt
)
def
on_attempt_deleted
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Archive the exam attempt when the item is about to be deleted
Make a clone and populate in the History table
"""
archive_object
=
ProctoredExamStudentAttemptHistory
(
user
=
instance
.
user
,
proctored_exam
=
instance
.
proctored_exam
,
started_at
=
instance
.
started_at
,
completed_at
=
instance
.
completed_at
,
attempt_code
=
instance
.
attempt_code
,
external_id
=
instance
.
external_id
,
allowed_time_limit_mins
=
instance
.
allowed_time_limit_mins
,
status
=
instance
.
status
,
taking_as_proctored
=
instance
.
taking_as_proctored
,
is_sample_attempt
=
instance
.
is_sample_attempt
,
student_name
=
instance
.
student_name
)
archive_object
.
save
()
class
QuerySetWithUpdateOverride
(
models
.
query
.
QuerySet
):
class
QuerySetWithUpdateOverride
(
models
.
query
.
QuerySet
):
...
...
edx_proctoring/serializers.py
View file @
34cd882f
...
@@ -66,8 +66,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
...
@@ -66,8 +66,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
"""
"""
Serializer for the ProctoredExamStudentAttempt Model.
Serializer for the ProctoredExamStudentAttempt Model.
"""
"""
proctored_exam
_id
=
serializers
.
IntegerField
(
source
=
"proctored_exam_id"
)
proctored_exam
=
ProctoredExamSerializer
(
)
user
_id
=
serializers
.
IntegerField
(
required
=
False
)
user
=
UserSerializer
(
)
class
Meta
:
class
Meta
:
"""
"""
...
@@ -76,8 +76,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
...
@@ -76,8 +76,8 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
model
=
ProctoredExamStudentAttempt
model
=
ProctoredExamStudentAttempt
fields
=
(
fields
=
(
"id"
,
"created"
,
"modified"
,
"user
_id
"
,
"started_at"
,
"completed_at"
,
"id"
,
"created"
,
"modified"
,
"user"
,
"started_at"
,
"completed_at"
,
"external_id"
,
"status"
,
"proctored_exam
_id
"
,
"allowed_time_limit_mins"
,
"external_id"
,
"status"
,
"proctored_exam"
,
"allowed_time_limit_mins"
,
"attempt_code"
,
"is_sample_attempt"
,
"taking_as_proctored"
"attempt_code"
,
"is_sample_attempt"
,
"taking_as_proctored"
)
)
...
...
edx_proctoring/static/proctoring/js/collections/proctored_exam_attempt_collection.js
0 → 100644
View file @
34cd882f
var
edx
=
edx
||
{};
(
function
(
Backbone
)
{
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
edx
.
instructor_dashboard
.
proctoring
=
edx
.
instructor_dashboard
.
proctoring
||
{};
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptCollection
=
Backbone
.
Collection
.
extend
({
/* model for a collection of ProctoredExamAllowance */
model
:
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptModel
,
url
:
'/api/edx_proctoring/v1/proctored_exam/attempt/course_id/'
});
this
.
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptCollection
=
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptCollection
;
}).
call
(
this
,
Backbone
);
\ No newline at end of file
edx_proctoring/static/proctoring/js/models/proctored_exam_attempt_model.js
0 → 100644
View file @
34cd882f
var
edx
=
edx
||
{};
(
function
(
Backbone
)
{
'use strict'
;
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
edx
.
instructor_dashboard
.
proctoring
=
edx
.
instructor_dashboard
.
proctoring
||
{};
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptModel
=
Backbone
.
Model
.
extend
({
url
:
'/api/edx_proctoring/v1/proctored_exam/attempt/'
});
this
.
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptModel
=
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptModel
;
}).
call
(
this
,
Backbone
);
edx_proctoring/static/proctoring/js/proctored_app.js
View file @
34cd882f
...
@@ -5,4 +5,10 @@ $(function() {
...
@@ -5,4 +5,10 @@ $(function() {
model
:
new
ProctoredExamModel
()
model
:
new
ProctoredExamModel
()
});
});
proctored_exam_view
.
render
();
proctored_exam_view
.
render
();
var
proctored_exam_attempt_view
=
new
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptView
({
el
:
$
(
'.student-proctored-exam-container'
),
template_url
:
'/static/proctoring/templates/student-proctored-exam-attempts.underscore'
,
collection
:
new
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptCollection
(),
model
:
new
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptModel
()
});
});
});
edx_proctoring/static/proctoring/js/views/proctored_exam_attempt_view.js
0 → 100644
View file @
34cd882f
var
edx
=
edx
||
{};
(
function
(
Backbone
,
$
,
_
)
{
'use strict'
;
edx
.
instructor_dashboard
=
edx
.
instructor_dashboard
||
{};
edx
.
instructor_dashboard
.
proctoring
=
edx
.
instructor_dashboard
.
proctoring
||
{};
var
viewHelper
=
{
getDateFormat
:
function
(
date
)
{
if
(
date
)
{
return
new
Date
(
date
).
toString
(
'MMM dd, yyyy h:mmtt'
);
}
else
{
return
'N/A'
;
}
}
};
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptView
=
Backbone
.
View
.
extend
({
initialize
:
function
(
options
)
{
this
.
$el
=
options
.
el
;
this
.
collection
=
options
.
collection
;
this
.
tempate_url
=
options
.
template_url
;
this
.
model
=
options
.
model
;
this
.
course_id
=
this
.
$el
.
data
(
'course-id'
);
this
.
template
=
null
;
this
.
initial_url
=
this
.
collection
.
url
;
this
.
attempt_url
=
this
.
model
.
url
;
this
.
collection
.
url
=
this
.
initial_url
+
this
.
course_id
;
this
.
inSearchMode
=
false
;
this
.
searchText
=
""
;
/* re-render if the model changes */
this
.
listenTo
(
this
.
collection
,
'change'
,
this
.
collectionChanged
);
/* Load the static template for rendering. */
this
.
loadTemplateData
();
},
events
:
{
"click .remove-attempt"
:
"onRemoveAttempt"
,
'click li > a.target-link'
:
'getPaginatedAttempts'
,
'click .search-attempts > span.search'
:
'searchAttempts'
,
'click .search-attempts > span.clear-search'
:
'clearSearch'
},
searchAttempts
:
function
(
event
)
{
var
searchText
=
$
(
'#search_attempt_id'
).
val
();
if
(
searchText
!==
""
)
{
this
.
inSearchMode
=
true
;
this
.
searchText
=
searchText
;
this
.
collection
.
url
=
this
.
initial_url
+
this
.
course_id
+
"/search/"
+
searchText
;
this
.
hydrate
();
event
.
stopPropagation
();
event
.
preventDefault
();
}
},
clearSearch
:
function
(
event
)
{
this
.
inSearchMode
=
false
;
this
.
searchText
=
""
;
this
.
collection
.
url
=
this
.
initial_url
+
this
.
course_id
;
this
.
hydrate
();
event
.
stopPropagation
();
event
.
preventDefault
();
},
getPaginatedAttempts
:
function
(
event
)
{
var
target
=
$
(
event
.
currentTarget
);
this
.
collection
.
url
=
target
.
data
(
'target-url'
);
this
.
hydrate
();
event
.
stopPropagation
();
event
.
preventDefault
();
},
getCSRFToken
:
function
()
{
var
cookieValue
=
null
;
var
name
=
'csrftoken'
;
if
(
document
.
cookie
&&
document
.
cookie
!=
''
)
{
var
cookies
=
document
.
cookie
.
split
(
';'
);
for
(
var
i
=
0
;
i
<
cookies
.
length
;
i
++
)
{
var
cookie
=
jQuery
.
trim
(
cookies
[
i
]);
// Does this cookie string begin with the name we want?
if
(
cookie
.
substring
(
0
,
name
.
length
+
1
)
==
(
name
+
'='
))
{
cookieValue
=
decodeURIComponent
(
cookie
.
substring
(
name
.
length
+
1
));
break
;
}
}
}
return
cookieValue
;
},
loadTemplateData
:
function
()
{
var
self
=
this
;
$
.
ajax
({
url
:
self
.
tempate_url
,
dataType
:
"html"
})
.
error
(
function
(
jqXHR
,
textStatus
,
errorThrown
)
{
})
.
done
(
function
(
template_data
)
{
self
.
template
=
_
.
template
(
template_data
);
self
.
hydrate
();
});
},
hydrate
:
function
()
{
/* This function will load the bound collection */
/* add and remove a class when we do the initial loading */
/* we might - at some point - add a visual element to the */
/* loading, like a spinner */
var
self
=
this
;
self
.
collection
.
fetch
({
success
:
function
()
{
self
.
render
();
}
});
},
collectionChanged
:
function
()
{
this
.
hydrate
();
},
render
:
function
()
{
if
(
this
.
template
!==
null
)
{
var
self
=
this
;
var
data
=
{
proctored_exam_attempts
:
this
.
collection
.
toJSON
()[
0
].
proctored_exam_attempts
,
pagination_info
:
this
.
collection
.
toJSON
()[
0
].
pagination_info
,
attempt_url
:
this
.
collection
.
toJSON
()[
0
].
attempt_url
,
inSearchMode
:
this
.
inSearchMode
,
searchText
:
this
.
searchText
};
_
.
extend
(
data
,
viewHelper
);
var
html
=
this
.
template
(
data
);
this
.
$el
.
html
(
html
);
this
.
$el
.
show
();
}
},
onRemoveAttempt
:
function
(
event
)
{
event
.
preventDefault
();
var
$target
=
$
(
event
.
currentTarget
);
var
attemptId
=
$target
.
data
(
"attemptId"
);
var
self
=
this
;
self
.
model
.
url
=
this
.
attempt_url
+
attemptId
;
self
.
model
.
fetch
(
{
headers
:
{
"X-CSRFToken"
:
this
.
getCSRFToken
()
},
type
:
'DELETE'
,
success
:
function
()
{
// fetch the attempts again.
self
.
hydrate
();
}
});
}
});
this
.
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptView
=
edx
.
instructor_dashboard
.
proctoring
.
ProctoredExamAttemptView
;
}).
call
(
this
,
Backbone
,
$
,
_
);
edx_proctoring/static/proctoring/templates/student-proctored-exam-attempts.underscore
0 → 100644
View file @
34cd882f
<div class="wrapper-content wrapper">
<section class="content">
<div class="top-header">
<div class='search-attempts'>
<input type="text" id="search_attempt_id" placeholder="e.g johndoe or john.do@gmail.com"
<% if (inSearchMode) { %>
value="<%= searchText %>"
<%} %>
/>
<span class="search"><i class="fa fa-search"></i></span>
<span class="clear-search"><i class="fa fa-remove"></i></i></span>
</div>
<ul class="pagination">
<% if (!pagination_info.has_previous){ %>
<li class="disabled">
<a aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<% } else { %>
<li>
<a class="target-link " data-target-url="
<%- interpolate(
'%(attempt_url)s?page=%(count)s ',
{
attempt_url: attempt_url,
count: pagination_info.current_page - 1
},
true
) %> "
href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<% }%>
<% for(var n = 1; n <= pagination_info.total_pages; n++) { %>
<li>
<a class="target-link <% if (pagination_info.current_page == n){ %> active <% } %>"
data-target-url="
<%- interpolate(
'%(attempt_url)s?page=%(count)s ',
{
attempt_url: attempt_url,
count: n
},
true
) %>
"
href="#"><%= n %>
</a>
</li>
<% } %>
<% if (!pagination_info.has_next){ %>
<li class="disabled">
<a aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
<% } else { %>
<li>
<a class="target-link" href="#" aria-label="Next" data-target-url="
<%- interpolate(
'%(attempt_url)s?page=%(count)s ',
{
attempt_url: attempt_url,
count: pagination_info.current_page + 1
},
true
) %> "
>
<span aria-hidden="true">»</span>
</a>
</li>
<% }%>
</ul>
<div class="clearfix"></div>
</div>
<table class="exam-attempts-table">
<thead>
<tr class="exam-attempt-headings">
<th class="username"><%- gettext("Username") %></th>
<th class="exam-name"><%- gettext("Exam Name") %></th>
<th class="attempt-allowed-time"><%- gettext("Allowed Time (Minutes)") %> </th>
<th class="attempt-started-at"><%- gettext("Started At") %></th>
<th class="attempt-completed-at"><%- gettext("Completed At") %> </th>
<th class="attempt-status"><%- gettext("Status") %> </th>
<th class="c_action"><%- gettext("Action") %> </th>
</tr>
</thead>
<tbody>
<% _.each(proctored_exam_attempts, function(proctored_exam_attempt){ %>
<tr class="allowance-items">
<td>
<%- interpolate(gettext(' %(username)s '), { username: proctored_exam_attempt.user.username }, true) %>
</td>
<td>
<%- interpolate(gettext(' %(exam_display_name)s '), { exam_display_name: proctored_exam_attempt.proctored_exam.exam_name }, true) %>
</td>
<td> <%= proctored_exam_attempt.allowed_time_limit_mins %></td>
<td> <%= getDateFormat(proctored_exam_attempt.started_at) %></td>
<td> <%= getDateFormat(proctored_exam_attempt.completed_at) %></td>
<td>
<% if (proctored_exam_attempt.status){ %>
<%= proctored_exam_attempt.status %>
<% } else { %>
N/A
<% } %>
</td>
<td>
<% if (proctored_exam_attempt.status){ %>
<a href="#" class="remove-attempt" data-attempt-id="<%= proctored_exam_attempt.id %>" >[x]</a>
</td>
<% } else { %>
N/A
<% } %>
</tr>
<% }); %>
</tbody>
</table>
</section>
</div>
\ No newline at end of file
edx_proctoring/tests/test_api.py
View file @
34cd882f
...
@@ -21,8 +21,10 @@ from edx_proctoring.api import (
...
@@ -21,8 +21,10 @@ from edx_proctoring.api import (
get_student_view
,
get_student_view
,
get_allowances_for_course
,
get_allowances_for_course
,
get_all_exams_for_course
,
get_all_exams_for_course
,
get_exam_attempt_by_id
get_exam_attempt_by_id
,
)
remove_exam_attempt_by_id
,
get_all_exam_attempts
,
get_filtered_exam_attempts
)
from
edx_proctoring.exceptions
import
(
from
edx_proctoring.exceptions
import
(
ProctoredExamAlreadyExists
,
ProctoredExamAlreadyExists
,
ProctoredExamNotFoundException
,
ProctoredExamNotFoundException
,
...
@@ -288,8 +290,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -288,8 +290,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
_create_unstarted_exam_attempt
()
self
.
_create_unstarted_exam_attempt
()
exam_attempt
=
get_exam_attempt
(
self
.
proctored_exam_id
,
self
.
user_id
)
exam_attempt
=
get_exam_attempt
(
self
.
proctored_exam_id
,
self
.
user_id
)
self
.
assertEqual
(
exam_attempt
[
'proctored_exam
_
id'
],
self
.
proctored_exam_id
)
self
.
assertEqual
(
exam_attempt
[
'proctored_exam
'
][
'
id'
],
self
.
proctored_exam_id
)
self
.
assertEqual
(
exam_attempt
[
'user
_
id'
],
self
.
user_id
)
self
.
assertEqual
(
exam_attempt
[
'user
'
][
'
id'
],
self
.
user_id
)
def
test_start_uncreated_attempt
(
self
):
def
test_start_uncreated_attempt
(
self
):
"""
"""
...
@@ -336,6 +338,19 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -336,6 +338,19 @@ 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_remove_exam_attempt
(
self
):
"""
Calling the api remove function removes the attempt.
"""
with
self
.
assertRaises
(
StudentExamAttemptDoesNotExistsException
):
remove_exam_attempt_by_id
(
9999
)
proctored_exam_student_attempt
=
self
.
_create_unstarted_exam_attempt
()
remove_exam_attempt_by_id
(
proctored_exam_student_attempt
.
id
)
with
self
.
assertRaises
(
StudentExamAttemptDoesNotExistsException
):
remove_exam_attempt_by_id
(
proctored_exam_student_attempt
.
id
)
def
test_stop_a_non_started_exam
(
self
):
def
test_stop_a_non_started_exam
(
self
):
"""
"""
Stop an exam attempt that had not started yet.
Stop an exam attempt that had not started yet.
...
@@ -371,6 +386,47 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -371,6 +386,47 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
assertEqual
(
len
(
student_active_exams
[
0
][
'allowances'
]),
2
)
self
.
assertEqual
(
len
(
student_active_exams
[
0
][
'allowances'
]),
2
)
self
.
assertEqual
(
len
(
student_active_exams
[
1
][
'allowances'
]),
0
)
self
.
assertEqual
(
len
(
student_active_exams
[
1
][
'allowances'
]),
0
)
def
test_get_filtered_exam_attempts
(
self
):
"""
Test to get all the exams filtered by the course_id
and search type.
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
exam_id
=
create_exam
(
course_id
=
self
.
course_id
,
content_id
=
'test_content_2'
,
exam_name
=
'Final Test Exam'
,
time_limit_mins
=
self
.
default_time_limit
)
new_exam_attempt
=
create_exam_attempt
(
exam_id
=
exam_id
,
user_id
=
self
.
user_id
)
filtered_attempts
=
get_filtered_exam_attempts
(
self
.
course_id
,
self
.
user
.
username
)
self
.
assertEqual
(
len
(
filtered_attempts
),
2
)
self
.
assertEqual
(
filtered_attempts
[
0
][
'id'
],
exam_attempt
.
id
)
self
.
assertEqual
(
filtered_attempts
[
1
][
'id'
],
new_exam_attempt
)
def
test_get_all_exam_attempts
(
self
):
"""
Test to get all the exam attempts.
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
exam_id
=
create_exam
(
course_id
=
self
.
course_id
,
content_id
=
'test_content_2'
,
exam_name
=
'Final Test Exam'
,
time_limit_mins
=
self
.
default_time_limit
)
updated_exam_attempt_id
=
create_exam_attempt
(
exam_id
=
exam_id
,
user_id
=
self
.
user_id
)
all_exams
=
get_all_exam_attempts
(
self
.
course_id
)
self
.
assertEqual
(
len
(
all_exams
),
2
)
self
.
assertEqual
(
all_exams
[
0
][
'id'
],
exam_attempt
.
id
)
self
.
assertEqual
(
all_exams
[
1
][
'id'
],
updated_exam_attempt_id
)
def
test_get_student_view
(
self
):
def
test_get_student_view
(
self
):
"""
"""
Test for get_student_view promting the user to take the exam
Test for get_student_view promting the user to take the exam
...
...
edx_proctoring/tests/test_models.py
View file @
34cd882f
"""
"""
All tests for the models.py
All tests for the models.py
"""
"""
from
edx_proctoring.models
import
ProctoredExam
,
ProctoredExamStudentAllowance
,
ProctoredExamStudentAllowanceHistory
from
edx_proctoring.models
import
ProctoredExam
,
ProctoredExamStudentAllowance
,
ProctoredExamStudentAllowanceHistory
,
\
ProctoredExamStudentAttempt
,
ProctoredExamStudentAttemptHistory
from
.utils
import
(
from
.utils
import
(
LoggedInTestCase
LoggedInTestCase
...
@@ -104,3 +105,40 @@ class ProctoredExamModelTests(LoggedInTestCase):
...
@@ -104,3 +105,40 @@ class ProctoredExamModelTests(LoggedInTestCase):
proctored_exam_student_history
=
ProctoredExamStudentAllowanceHistory
.
objects
.
filter
(
user_id
=
1
)
proctored_exam_student_history
=
ProctoredExamStudentAllowanceHistory
.
objects
.
filter
(
user_id
=
1
)
self
.
assertEqual
(
len
(
proctored_exam_student_history
),
1
)
self
.
assertEqual
(
len
(
proctored_exam_student_history
),
1
)
class
ProctoredExamStudentAttemptTests
(
LoggedInTestCase
):
"""
Tests for the ProctoredExamStudentAttempt Model
"""
def
test_delete_proctored_exam_attempt
(
self
):
# pylint: disable=invalid-name
"""
Deleting the proctored exam attempt creates an entry in the history table.
"""
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'test_course'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
)
attempt
=
ProctoredExamStudentAttempt
.
objects
.
create
(
proctored_exam_id
=
proctored_exam
.
id
,
user_id
=
1
,
student_name
=
"John. D"
,
allowed_time_limit_mins
=
10
,
attempt_code
=
"123456"
,
taking_as_proctored
=
True
,
is_sample_attempt
=
True
,
external_id
=
1
)
# No entry in the History table on creation of the Allowance entry.
attempt_history
=
ProctoredExamStudentAttemptHistory
.
objects
.
filter
(
user_id
=
1
)
self
.
assertEqual
(
len
(
attempt_history
),
0
)
attempt
.
delete_exam_attempt
()
attempt_history
=
ProctoredExamStudentAttemptHistory
.
objects
.
filter
(
user_id
=
1
)
self
.
assertEqual
(
len
(
attempt_history
),
1
)
edx_proctoring/tests/test_views.py
View file @
34cd882f
# pylint: disable=too-many-lines
"""
"""
All tests for the proctored_exams.py
All tests for the proctored_exams.py
"""
"""
...
@@ -429,10 +430,50 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -429,10 +430,50 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'id'
],
attempt_id
)
self
.
assertEqual
(
response_data
[
'id'
],
attempt_id
)
self
.
assertEqual
(
response_data
[
'proctored_exam
_
id'
],
proctored_exam
.
id
)
self
.
assertEqual
(
response_data
[
'proctored_exam
'
][
'
id'
],
proctored_exam
.
id
)
self
.
assertIsNotNone
(
response_data
[
'started_at'
])
self
.
assertIsNotNone
(
response_data
[
'started_at'
])
self
.
assertIsNone
(
response_data
[
'completed_at'
])
self
.
assertIsNone
(
response_data
[
'completed_at'
])
def
test_remove_attempt
(
self
):
"""
Confirms that an attempt can be removed
"""
# 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
)
response
=
self
.
client
.
delete
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
1
])
)
self
.
assertEqual
(
response
.
status_code
,
400
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
,
'start_clock'
:
True
,
}
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
)
attempt_id
=
response_data
[
'exam_attempt_id'
]
self
.
assertGreater
(
attempt_id
,
0
)
response
=
self
.
client
.
delete
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_read_others_attempt
(
self
):
def
test_read_others_attempt
(
self
):
"""
"""
Confirms that we cnanot read someone elses attempt
Confirms that we cnanot read someone elses attempt
...
@@ -547,6 +588,88 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -547,6 +588,88 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response_data
=
json
.
loads
(
response
.
content
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'exam_attempt_id'
],
old_attempt_id
)
self
.
assertEqual
(
response_data
[
'exam_attempt_id'
],
old_attempt_id
)
def
test_get_exam_attempts
(
self
):
"""
Test to get the exam attempts in a course.
"""
# 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
)
url
=
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
kwargs
=
{
'course_id'
:
proctored_exam
.
course_id
})
self
.
assertEqual
(
response
.
status_code
,
200
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
len
(
response_data
[
'proctored_exam_attempts'
]),
1
)
url
=
'{url}?page={invalid_page_no}'
.
format
(
url
=
url
,
invalid_page_no
=
9999
)
# url with the invalid page # still gives us the first page result.
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
len
(
response_data
[
'proctored_exam_attempts'
]),
1
)
def
test_get_filtered_exam_attempts
(
self
):
"""
Test to get the exam attempts in a course.
"""
# 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
,
'start_clock'
:
False
,
'attempt_proctored'
:
False
}
# create a exam attempt
response
=
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
attempt_data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
client
.
login_user
(
self
.
second_user
)
# create a new exam attempt for second student
response
=
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
attempt_data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
client
.
login_user
(
self
.
user
)
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt.search'
,
kwargs
=
{
'course_id'
:
proctored_exam
.
course_id
,
'search_by'
:
'tester'
}
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
len
(
response_data
[
'proctored_exam_attempts'
]),
2
)
def
test_stop_others_attempt
(
self
):
def
test_stop_others_attempt
(
self
):
"""
"""
Start an exam (create an exam attempt)
Start an exam (create an exam attempt)
...
...
edx_proctoring/urls.py
View file @
34cd882f
...
@@ -36,6 +36,17 @@ urlpatterns = patterns( # pylint: disable=invalid-name
...
@@ -36,6 +36,17 @@ urlpatterns = patterns( # pylint: disable=invalid-name
name
=
'edx_proctoring.proctored_exam.attempt'
name
=
'edx_proctoring.proctored_exam.attempt'
),
),
url
(
url
(
r'edx_proctoring/v1/proctored_exam/attempt/course_id/{}$'
.
format
(
settings
.
COURSE_ID_PATTERN
),
views
.
StudentProctoredExamAttemptCollection
.
as_view
(),
name
=
'edx_proctoring.proctored_exam.attempt'
),
url
(
r'edx_proctoring/v1/proctored_exam/attempt/course_id/{}/search/(?P<search_by>.+)$'
.
format
(
settings
.
COURSE_ID_PATTERN
),
views
.
StudentProctoredExamAttemptCollection
.
as_view
(),
name
=
'edx_proctoring.proctored_exam.attempt.search'
),
url
(
r'edx_proctoring/v1/proctored_exam/attempt$'
,
r'edx_proctoring/v1/proctored_exam/attempt$'
,
views
.
StudentProctoredExamAttemptCollection
.
as_view
(),
views
.
StudentProctoredExamAttemptCollection
.
as_view
(),
name
=
'edx_proctoring.proctored_exam.attempt.collection'
name
=
'edx_proctoring.proctored_exam.attempt.collection'
...
...
edx_proctoring/views.py
View file @
34cd882f
...
@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse, NoReverseMatch
...
@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse, NoReverseMatch
from
rest_framework
import
status
from
rest_framework
import
status
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
django.core.paginator
import
Paginator
,
EmptyPage
,
PageNotAnInteger
from
edx_proctoring.api
import
(
from
edx_proctoring.api
import
(
create_exam
,
create_exam
,
update_exam
,
update_exam
,
...
@@ -26,7 +27,9 @@ from edx_proctoring.api import (
...
@@ -26,7 +27,9 @@ from edx_proctoring.api import (
get_allowances_for_course
,
get_allowances_for_course
,
get_all_exams_for_course
,
get_all_exams_for_course
,
get_exam_attempt_by_id
,
get_exam_attempt_by_id
,
)
get_all_exam_attempts
,
remove_exam_attempt_by_id
,
get_filtered_exam_attempts
)
from
edx_proctoring.exceptions
import
(
from
edx_proctoring.exceptions
import
(
ProctoredBaseException
,
ProctoredBaseException
,
ProctoredExamNotFoundException
,
ProctoredExamNotFoundException
,
...
@@ -38,6 +41,8 @@ from edx_proctoring.serializers import ProctoredExamSerializer
...
@@ -38,6 +41,8 @@ from edx_proctoring.serializers import ProctoredExamSerializer
from
.utils
import
AuthenticatedAPIView
from
.utils
import
AuthenticatedAPIView
ATTEMPTS_PER_PAGE
=
25
LOG
=
logging
.
getLogger
(
"edx_proctoring_views"
)
LOG
=
logging
.
getLogger
(
"edx_proctoring_views"
)
...
@@ -251,7 +256,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -251,7 +256,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
# make sure the the attempt belongs to the calling user_id
# make sure the the attempt belongs to the calling user_id
if
attempt
[
'user
_
id'
]
!=
request
.
user
.
id
:
if
attempt
[
'user
'
][
'
id'
]
!=
request
.
user
.
id
:
err_msg
=
(
err_msg
=
(
'Attempted to access attempt_id {attempt_id} but '
'Attempted to access attempt_id {attempt_id} but '
'does not have access to it.'
.
format
(
'does not have access to it.'
.
format
(
...
@@ -288,7 +293,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -288,7 +293,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
# make sure the the attempt belongs to the calling user_id
# make sure the the attempt belongs to the calling user_id
if
attempt
[
'user
_
id'
]
!=
request
.
user
.
id
:
if
attempt
[
'user
'
][
'
id'
]
!=
request
.
user
.
id
:
err_msg
=
(
err_msg
=
(
'Attempted to access attempt_id {attempt_id} but '
'Attempted to access attempt_id {attempt_id} but '
'does not have access to it.'
.
format
(
'does not have access to it.'
.
format
(
...
@@ -298,7 +303,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -298,7 +303,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
raise
ProctoredExamPermissionDenied
(
err_msg
)
raise
ProctoredExamPermissionDenied
(
err_msg
)
exam_attempt_id
=
stop_exam_attempt
(
exam_attempt_id
=
stop_exam_attempt
(
exam_id
=
attempt
[
'proctored_exam
_
id'
],
exam_id
=
attempt
[
'proctored_exam
'
][
'
id'
],
user_id
=
request
.
user
.
id
user_id
=
request
.
user
.
id
)
)
return
Response
({
"exam_attempt_id"
:
exam_attempt_id
})
return
Response
({
"exam_attempt_id"
:
exam_attempt_id
})
...
@@ -309,6 +314,32 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -309,6 +314,32 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
data
=
{
"detail"
:
str
(
ex
)}
data
=
{
"detail"
:
str
(
ex
)}
)
)
@method_decorator
(
require_staff
)
def
delete
(
self
,
request
,
attempt_id
):
# pylint: disable=unused-argument
"""
HTTP DELETE handler. Removes an exam attempt.
"""
try
:
attempt
=
get_exam_attempt_by_id
(
attempt_id
)
if
not
attempt
:
err_msg
=
(
'Attempted to access attempt_id {attempt_id} but '
'it does not exist.'
.
format
(
attempt_id
=
attempt_id
)
)
raise
StudentExamAttemptDoesNotExistsException
(
err_msg
)
remove_exam_attempt_by_id
(
attempt_id
)
return
Response
()
except
ProctoredBaseException
,
ex
:
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
,
data
=
{
"detail"
:
str
(
ex
)}
)
class
StudentProctoredExamAttemptCollection
(
AuthenticatedAPIView
):
class
StudentProctoredExamAttemptCollection
(
AuthenticatedAPIView
):
"""
"""
...
@@ -358,10 +389,44 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
...
@@ -358,10 +389,44 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
return the status of the exam attempt
return the status of the exam attempt
"""
"""
def
get
(
self
,
request
):
def
get
(
self
,
request
,
course_id
=
None
,
search_by
=
None
):
# pylint: disable=unused-argument
"""
"""
HTTP GET Handler. Returns the status of the exam attempt.
HTTP GET Handler. Returns the status of the exam attempt.
"""
"""
if
course_id
is
not
None
:
if
search_by
is
not
None
:
exam_attempts
=
get_filtered_exam_attempts
(
course_id
,
search_by
)
attempt_url
=
reverse
(
'edx_proctoring.proctored_exam.attempt.search'
,
args
=
[
course_id
,
search_by
])
else
:
exam_attempts
=
get_all_exam_attempts
(
course_id
)
attempt_url
=
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
course_id
])
paginator
=
Paginator
(
exam_attempts
,
ATTEMPTS_PER_PAGE
)
page
=
request
.
GET
.
get
(
'page'
)
try
:
exam_attempts_page
=
paginator
.
page
(
page
)
except
PageNotAnInteger
:
# If page is not an integer, deliver first page.
exam_attempts_page
=
paginator
.
page
(
1
)
except
EmptyPage
:
# If page is out of range (e.g. 9999), deliver last page of results.
exam_attempts_page
=
paginator
.
page
(
paginator
.
num_pages
)
data
=
{
'proctored_exam_attempts'
:
exam_attempts_page
.
object_list
,
'pagination_info'
:
{
'has_previous'
:
exam_attempts_page
.
has_previous
(),
'has_next'
:
exam_attempts_page
.
has_next
(),
'current_page'
:
exam_attempts_page
.
number
,
'total_pages'
:
exam_attempts_page
.
paginator
.
num_pages
,
},
'attempt_url'
:
attempt_url
}
return
Response
(
data
=
data
,
status
=
status
.
HTTP_200_OK
)
exams
=
get_active_exams_for_user
(
request
.
user
.
id
)
exams
=
get_active_exams_for_user
(
request
.
user
.
id
)
...
...
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