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
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
780 additions
and
57 deletions
+780
-57
edx_proctoring/api.py
+41
-7
edx_proctoring/migrations/0003_auto__add_proctoredexamstudentattempthistory.py
+0
-0
edx_proctoring/models.py
+123
-35
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
):
"""
"""
Returns the Student Exam Attempt object if found
This should be the same schema as ProctoredExamStudentAttempt
else Returns None
.
but will record (for audit history) all entries that have been updated
.
"""
"""
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
)
# 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
"""
"""
Returns the active student exams (user in-progress exams)
Archive the exam attempt when the item is about to be deleted
Make a clone and populate in the History table
"""
"""
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
)
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