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
8a1a8152
Commit
8a1a8152
authored
Oct 02, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #162 from edx/muhhshoaib/SOL-1222-update-permissions-in-edx-proctoring
SOL-1222
parents
8aceb2ef
4510a7ca
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
449 additions
and
34 deletions
+449
-34
edx_proctoring/api.py
+6
-4
edx_proctoring/models.py
+23
-9
edx_proctoring/tests/test_api.py
+21
-3
edx_proctoring/tests/test_services.py
+12
-0
edx_proctoring/tests/test_views.py
+330
-8
edx_proctoring/views.py
+57
-10
No files found.
edx_proctoring/api.py
View file @
8a1a8152
...
@@ -199,11 +199,13 @@ def add_allowance_for_user(exam_id, user_info, key, value):
...
@@ -199,11 +199,13 @@ def add_allowance_for_user(exam_id, user_info, key, value):
ProctoredExamStudentAllowance
.
add_allowance_for_user
(
exam_id
,
user_info
,
key
,
value
)
ProctoredExamStudentAllowance
.
add_allowance_for_user
(
exam_id
,
user_info
,
key
,
value
)
def
get_allowances_for_course
(
course_id
):
def
get_allowances_for_course
(
course_id
,
timed_exams_only
):
"""
"""
Get all the allowances for the course.
Get all the allowances for the course.
"""
"""
student_allowances
=
ProctoredExamStudentAllowance
.
get_allowances_for_course
(
course_id
)
student_allowances
=
ProctoredExamStudentAllowance
.
get_allowances_for_course
(
course_id
,
timed_exams_only
=
timed_exams_only
)
return
[
ProctoredExamStudentAllowanceSerializer
(
allowance
)
.
data
for
allowance
in
student_allowances
]
return
[
ProctoredExamStudentAllowanceSerializer
(
allowance
)
.
data
for
allowance
in
student_allowances
]
...
@@ -803,7 +805,7 @@ def remove_exam_attempt(attempt_id):
...
@@ -803,7 +805,7 @@ def remove_exam_attempt(attempt_id):
)
)
def
get_all_exams_for_course
(
course_id
):
def
get_all_exams_for_course
(
course_id
,
timed_exams_only
):
"""
"""
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
of dictionaries, whose schema is the same as what is returned in
of dictionaries, whose schema is the same as what is returned in
...
@@ -827,7 +829,7 @@ def get_all_exams_for_course(course_id):
...
@@ -827,7 +829,7 @@ def get_all_exams_for_course(course_id):
..
..
]
]
"""
"""
exams
=
ProctoredExam
.
get_all_exams_for_course
(
course_id
)
exams
=
ProctoredExam
.
get_all_exams_for_course
(
course_id
,
timed_exams_only
=
timed_exams_only
)
return
[
ProctoredExamSerializer
(
proctored_exam
)
.
data
for
proctored_exam
in
exams
]
return
[
ProctoredExamSerializer
(
proctored_exam
)
.
data
for
proctored_exam
in
exams
]
...
...
edx_proctoring/models.py
View file @
8a1a8152
...
@@ -83,14 +83,18 @@ class ProctoredExam(TimeStampedModel):
...
@@ -83,14 +83,18 @@ class ProctoredExam(TimeStampedModel):
return
proctored_exam
return
proctored_exam
@classmethod
@classmethod
def
get_all_exams_for_course
(
cls
,
course_id
,
active_only
=
False
):
def
get_all_exams_for_course
(
cls
,
course_id
,
active_only
=
False
,
timed_exams_only
=
False
):
"""
"""
Returns all exams for a give course
Returns all exams for a give course
"""
"""
result
=
cls
.
objects
.
filter
(
course_id
=
course_id
)
filtered_query
=
Q
(
course_id
=
course_id
)
if
active_only
:
if
active_only
:
result
=
result
.
filter
(
is_active
=
True
)
filtered_query
=
filtered_query
&
Q
(
is_active
=
True
)
return
result
if
timed_exams_only
:
filtered_query
=
filtered_query
&
Q
(
is_proctored
=
False
)
return
cls
.
objects
.
filter
(
filtered_query
)
class
ProctoredExamStudentAttemptStatus
(
object
):
class
ProctoredExamStudentAttemptStatus
(
object
):
...
@@ -355,20 +359,26 @@ class ProctoredExamStudentAttemptManager(models.Manager):
...
@@ -355,20 +359,26 @@ class ProctoredExamStudentAttemptManager(models.Manager):
exam_attempt_obj
=
None
exam_attempt_obj
=
None
return
exam_attempt_obj
return
exam_attempt_obj
def
get_all_exam_attempts
(
self
,
course_id
):
def
get_all_exam_attempts
(
self
,
course_id
,
timed_exams_only
=
False
):
"""
"""
Returns the Student Exam Attempts for the given course_id.
Returns the Student Exam Attempts for the given course_id.
"""
"""
filtered_query
=
Q
(
proctored_exam__course_id
=
course_id
)
return
self
.
filter
(
proctored_exam__course_id
=
course_id
)
.
order_by
(
'-created'
)
if
timed_exams_only
:
filtered_query
=
filtered_query
&
Q
(
proctored_exam__is_proctored
=
False
)
def
get_filtered_exam_attempts
(
self
,
course_id
,
search_by
):
return
self
.
filter
(
filtered_query
)
.
order_by
(
'-created'
)
def
get_filtered_exam_attempts
(
self
,
course_id
,
search_by
,
timed_exams_only
=
False
):
"""
"""
Returns the Student Exam Attempts for the given course_id filtered by 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
)
&
(
filtered_query
=
Q
(
proctored_exam__course_id
=
course_id
)
&
(
Q
(
user__username__contains
=
search_by
)
|
Q
(
user__email__contains
=
search_by
)
Q
(
user__username__contains
=
search_by
)
|
Q
(
user__email__contains
=
search_by
)
)
)
if
timed_exams_only
:
filtered_query
=
filtered_query
&
Q
(
proctored_exam__is_proctored
=
False
)
return
self
.
filter
(
filtered_query
)
.
order_by
(
'-created'
)
return
self
.
filter
(
filtered_query
)
.
order_by
(
'-created'
)
...
@@ -605,11 +615,15 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
...
@@ -605,11 +615,15 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
verbose_name
=
'proctored allowance'
verbose_name
=
'proctored allowance'
@classmethod
@classmethod
def
get_allowances_for_course
(
cls
,
course_id
):
def
get_allowances_for_course
(
cls
,
course_id
,
timed_exams_only
=
False
):
"""
"""
Returns all the allowances for a course.
Returns all the allowances for a course.
"""
"""
return
cls
.
objects
.
filter
(
proctored_exam__course_id
=
course_id
)
filtered_query
=
Q
(
proctored_exam__course_id
=
course_id
)
if
timed_exams_only
:
filtered_query
=
filtered_query
&
Q
(
proctored_exam__is_proctored
=
False
)
return
cls
.
objects
.
filter
(
filtered_query
)
@classmethod
@classmethod
def
get_allowance_for_user
(
cls
,
exam_id
,
user_id
,
key
):
def
get_allowance_for_user
(
cls
,
exam_id
,
user_id
,
key
):
...
...
edx_proctoring/tests/test_api.py
View file @
8a1a8152
...
@@ -117,7 +117,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -117,7 +117,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
footer_msg
=
'About Proctored Exams'
self
.
footer_msg
=
'About Proctored Exams'
set_runtime_service
(
'credit'
,
MockCreditService
())
set_runtime_service
(
'credit'
,
MockCreditService
())
set_runtime_service
(
'instructor'
,
MockInstructorService
())
set_runtime_service
(
'instructor'
,
MockInstructorService
(
is_user_course_staff
=
True
))
def
_create_proctored_exam
(
self
):
def
_create_proctored_exam
(
self
):
"""
"""
...
@@ -305,9 +305,27 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -305,9 +305,27 @@ class ProctoredExamApiTests(LoggedInTestCase):
self
.
assertEqual
(
proctored_exam
[
'content_id'
],
self
.
content_id
)
self
.
assertEqual
(
proctored_exam
[
'content_id'
],
self
.
content_id
)
self
.
assertEqual
(
proctored_exam
[
'exam_name'
],
self
.
exam_name
)
self
.
assertEqual
(
proctored_exam
[
'exam_name'
],
self
.
exam_name
)
exams
=
get_all_exams_for_course
(
self
.
course_id
)
exams
=
get_all_exams_for_course
(
self
.
course_id
,
False
)
self
.
assertEqual
(
len
(
exams
),
4
)
self
.
assertEqual
(
len
(
exams
),
4
)
def
test_get_timed_exam
(
self
):
"""
test to get the exam by the exam_id and
then compare their values.
"""
timed_exam
=
get_exam_by_id
(
self
.
timed_exam
)
self
.
assertEqual
(
timed_exam
[
'course_id'
],
self
.
course_id
)
self
.
assertEqual
(
timed_exam
[
'content_id'
],
self
.
content_id_timed
)
self
.
assertEqual
(
timed_exam
[
'exam_name'
],
self
.
exam_name
)
timed_exam
=
get_exam_by_content_id
(
self
.
course_id
,
self
.
content_id_timed
)
self
.
assertEqual
(
timed_exam
[
'course_id'
],
self
.
course_id
)
self
.
assertEqual
(
timed_exam
[
'content_id'
],
self
.
content_id_timed
)
self
.
assertEqual
(
timed_exam
[
'exam_name'
],
self
.
exam_name
)
exams
=
get_all_exams_for_course
(
self
.
course_id
,
True
)
self
.
assertEqual
(
len
(
exams
),
1
)
def
test_get_invalid_proctored_exam
(
self
):
def
test_get_invalid_proctored_exam
(
self
):
"""
"""
test to get the exam by the invalid exam_id which will
test to get the exam by the invalid exam_id which will
...
@@ -356,7 +374,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -356,7 +374,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
Test to get all the allowances for a course.
Test to get all the allowances for a course.
"""
"""
allowance
=
self
.
_add_allowance_for_user
()
allowance
=
self
.
_add_allowance_for_user
()
course_allowances
=
get_allowances_for_course
(
self
.
course_id
)
course_allowances
=
get_allowances_for_course
(
self
.
course_id
,
False
)
self
.
assertEqual
(
len
(
course_allowances
),
1
)
self
.
assertEqual
(
len
(
course_allowances
),
1
)
self
.
assertEqual
(
course_allowances
[
0
][
'proctored_exam'
][
'course_id'
],
allowance
.
proctored_exam
.
course_id
)
self
.
assertEqual
(
course_allowances
[
0
][
'proctored_exam'
][
'course_id'
],
allowance
.
proctored_exam
.
course_id
)
...
...
edx_proctoring/tests/test_services.py
View file @
8a1a8152
...
@@ -86,12 +86,24 @@ class MockInstructorService(object):
...
@@ -86,12 +86,24 @@ class MockInstructorService(object):
"""
"""
Simple mock of the Instructor Service
Simple mock of the Instructor Service
"""
"""
def
__init__
(
self
,
is_user_course_staff
=
True
):
"""
Initializer
"""
self
.
is_user_course_staff
=
is_user_course_staff
def
delete_student_attempt
(
self
,
student_identifier
,
course_id
,
content_id
):
# pylint: disable=unused-argument
def
delete_student_attempt
(
self
,
student_identifier
,
course_id
,
content_id
):
# pylint: disable=unused-argument
"""
"""
Mock implementation
Mock implementation
"""
"""
return
True
return
True
def
is_course_staff
(
self
,
user
,
course_id
):
"""
Mocked implementation of is_course_staff
"""
return
self
.
is_user_course_staff
class
TestProctoringService
(
unittest
.
TestCase
):
class
TestProctoringService
(
unittest
.
TestCase
):
"""
"""
...
...
edx_proctoring/tests/test_views.py
View file @
8a1a8152
...
@@ -20,7 +20,7 @@ from edx_proctoring.models import (
...
@@ -20,7 +20,7 @@ from edx_proctoring.models import (
ProctoredExamStudentAllowance
,
ProctoredExamStudentAllowance
,
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttemptStatus
,
)
)
from
edx_proctoring.views
import
require_staff
from
edx_proctoring.views
import
require_staff
,
require_course_or_global_staff
from
edx_proctoring.api
import
(
from
edx_proctoring.api
import
(
create_exam
,
create_exam
,
create_exam_attempt
,
create_exam_attempt
,
...
@@ -35,7 +35,7 @@ from .utils import (
...
@@ -35,7 +35,7 @@ from .utils import (
from
edx_proctoring.urls
import
urlpatterns
from
edx_proctoring.urls
import
urlpatterns
from
edx_proctoring.backends.tests.test_review_payload
import
TEST_REVIEW_PAYLOAD
from
edx_proctoring.backends.tests.test_review_payload
import
TEST_REVIEW_PAYLOAD
from
edx_proctoring.backends.tests.test_software_secure
import
mock_response_content
from
edx_proctoring.backends.tests.test_software_secure
import
mock_response_content
from
edx_proctoring.tests.test_services
import
MockCreditService
from
edx_proctoring.tests.test_services
import
MockCreditService
,
MockInstructorService
from
edx_proctoring.runtime
import
set_runtime_service
,
get_runtime_service
from
edx_proctoring.runtime
import
set_runtime_service
,
get_runtime_service
...
@@ -204,11 +204,23 @@ class ProctoredExamViewTests(LoggedInTestCase):
...
@@ -204,11 +204,23 @@ class ProctoredExamViewTests(LoggedInTestCase):
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertFalse
(
func
.
called
)
self
.
assertFalse
(
func
.
called
)
def
test_decorator_require_course_or_global_staff
(
self
):
# pylint: disable=invalid-name
"""
Test assert require_course_or_global_staff before hitting any api url.
"""
func
=
Mock
()
decorated_func
=
require_course_or_global_staff
(
func
)
request
=
self
.
mock_request
()
response
=
decorated_func
(
request
)
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertFalse
(
func
.
called
)
def
mock_request
(
self
):
def
mock_request
(
self
):
"""
"""
mock request
mock request
"""
"""
request
=
Mock
()
request
=
Mock
()
request
.
DATA
=
{}
self
.
user
.
is_staff
=
False
self
.
user
.
is_staff
=
False
self
.
user
.
save
()
self
.
user
.
save
()
request
.
user
=
self
.
user
request
.
user
=
self
.
user
...
@@ -393,6 +405,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -393,6 +405,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self
.
student_taking_exam
=
User
()
self
.
student_taking_exam
=
User
()
self
.
student_taking_exam
.
save
()
self
.
student_taking_exam
.
save
()
set_runtime_service
(
'instructor'
,
MockInstructorService
(
is_user_course_staff
=
True
))
def
test_start_exam_create
(
self
):
def
test_start_exam_create
(
self
):
"""
"""
Start an exam (create an exam attempt)
Start an exam (create an exam attempt)
...
@@ -772,12 +786,57 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -772,12 +786,57 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
attempt_id
=
response_data
[
'exam_attempt_id'
]
attempt_id
=
response_data
[
'exam_attempt_id'
]
self
.
assertGreater
(
attempt_id
,
0
)
self
.
assertGreater
(
attempt_id
,
0
)
self
.
user
.
is_staff
=
False
self
.
user
.
save
()
response
=
self
.
client
.
delete
(
response
=
self
.
client
.
delete
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_remove_attempt_non_staff
(
self
):
"""
Confirms that an attempt cannot be removed
by the not staff/global user
"""
# Create an exam.
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
,
'start_clock'
:
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
)
# now set the user is_staff to False
# and also user is not a course staff
self
.
user
.
is_staff
=
False
self
.
user
.
save
()
set_runtime_service
(
'instructor'
,
MockInstructorService
(
is_user_course_staff
=
False
))
response
=
self
.
client
.
delete
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
)
self
.
assertEqual
(
response
.
status_code
,
403
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'detail'
],
'Must be a Staff User to Perform this request.'
)
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
...
@@ -1000,22 +1059,44 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -1000,22 +1059,44 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
def
test_exam_attempts_not_staff
(
self
):
def
test_exam_attempts_not_staff
(
self
):
"""
"""
Test to get the exam attempts in a course.
Test to get the exam attempts in a course as a not
staff user but still we get the timed exams attempts
but not the proctored exam attempts
"""
"""
# Create an exam.
# Create an
timed_
exam.
proctor
ed_exam
=
ProctoredExam
.
objects
.
create
(
tim
ed_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
time_limit_mins
=
90
,
is_proctored
=
False
)
attempt_data
=
{
'exam_id'
:
timed_exam
.
id
,
'user_id'
:
self
.
student_taking_exam
.
id
,
'external_id'
:
timed_exam
.
external_id
}
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
attempt_data
)
# Create a proctored exam.
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content1'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
,
is_proctored
=
True
)
)
attempt_data
=
{
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'exam_id'
:
proctored_exam
.
id
,
'user_id'
:
self
.
student_taking_exam
.
id
,
'user_id'
:
self
.
student_taking_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
'external_id'
:
proctored_exam
.
external_id
}
}
response
=
self
.
client
.
post
(
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
attempt_data
attempt_data
)
)
...
@@ -1025,7 +1106,15 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -1025,7 +1106,15 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self
.
user
.
save
()
self
.
user
.
save
()
response
=
self
.
client
.
get
(
url
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
# we should only get the timed exam attempt in this case
# so the len should be 1
self
.
assertEqual
(
len
(
response_data
[
'proctored_exam_attempts'
]),
1
)
self
.
assertEqual
(
response_data
[
'proctored_exam_attempts'
][
0
][
'proctored_exam'
][
'is_proctored'
],
timed_exam
.
is_proctored
)
def
test_get_filtered_exam_attempts
(
self
):
def
test_get_filtered_exam_attempts
(
self
):
"""
"""
...
@@ -1081,6 +1170,63 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -1081,6 +1170,63 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self
.
assertEqual
(
attempt
[
'proctored_exam'
][
'id'
],
proctored_exam
.
id
)
self
.
assertEqual
(
attempt
[
'proctored_exam'
][
'id'
],
proctored_exam
.
id
)
self
.
assertEqual
(
attempt
[
'user'
][
'id'
],
self
.
user
.
id
)
self
.
assertEqual
(
attempt
[
'user'
][
'id'
],
self
.
user
.
id
)
def
test_get_filtered_timed_exam_attempts
(
self
):
# pylint: disable=invalid-name
"""
Test to get the filtered timed exam attempts in a course.
"""
# Create an exam.
timed_exm
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
,
is_proctored
=
False
)
attempt_data
=
{
'exam_id'
:
timed_exm
.
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
.
user
.
is_staff
=
False
self
.
user
.
save
()
self
.
client
.
login_user
(
self
.
user
)
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempts.search'
,
kwargs
=
{
'course_id'
:
timed_exm
.
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
)
attempt
=
response_data
[
'proctored_exam_attempts'
][
0
]
self
.
assertEqual
(
attempt
[
'proctored_exam'
][
'id'
],
timed_exm
.
id
)
self
.
assertEqual
(
attempt
[
'user'
][
'id'
],
self
.
second_user
.
id
)
attempt
=
response_data
[
'proctored_exam_attempts'
][
1
]
self
.
assertEqual
(
attempt
[
'proctored_exam'
][
'id'
],
timed_exm
.
id
)
self
.
assertEqual
(
attempt
[
'user'
][
'id'
],
self
.
user
.
id
)
def
test_paginated_exam_attempts
(
self
):
def
test_paginated_exam_attempts
(
self
):
"""
"""
Test to get the paginated exam attempts in a course.
Test to get the paginated exam attempts in a course.
...
@@ -1209,6 +1355,48 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -1209,6 +1355,48 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
# make sure we have the accessible human string
# make sure we have the accessible human string
self
.
assertEqual
(
data
[
'accessibility_time_string'
],
'you have 1 hour and 30 minutes remaining'
)
self
.
assertEqual
(
data
[
'accessibility_time_string'
],
'you have 1 hour and 30 minutes remaining'
)
def
test_get_exam_attempt_with_non_staff_user
(
self
):
# pylint: disable=invalid-name
"""
Test Case for retrieving student proctored exam attempt status.
"""
# 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
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
data
=
json
.
loads
(
response
.
content
)
self
.
assertNotIn
(
'exam_display_name'
,
data
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'user_id'
:
self
.
user
.
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
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'exam_display_name'
],
'Test Exam'
)
self
.
assertEqual
(
data
[
'low_threshold_sec'
],
1080
)
self
.
assertEqual
(
data
[
'critically_low_threshold_sec'
],
270
)
# make sure we have the accessible human string
self
.
assertEqual
(
data
[
'accessibility_time_string'
],
'you have 1 hour and 30 minutes remaining'
)
def
test_get_expired_attempt
(
self
):
def
test_get_expired_attempt
(
self
):
"""
"""
Test Case for retrieving student proctored exam attempt status after it has expired
Test Case for retrieving student proctored exam attempt status after it has expired
...
@@ -1571,6 +1759,8 @@ class TestExamAllowanceView(LoggedInTestCase):
...
@@ -1571,6 +1759,8 @@ class TestExamAllowanceView(LoggedInTestCase):
self
.
student_taking_exam
=
User
()
self
.
student_taking_exam
=
User
()
self
.
student_taking_exam
.
save
()
self
.
student_taking_exam
.
save
()
set_runtime_service
(
'instructor'
,
MockInstructorService
(
is_user_course_staff
=
True
))
def
test_add_allowance_for_user
(
self
):
def
test_add_allowance_for_user
(
self
):
"""
"""
Add allowance for a user for an exam.
Add allowance for a user for an exam.
...
@@ -1654,6 +1844,37 @@ class TestExamAllowanceView(LoggedInTestCase):
...
@@ -1654,6 +1844,37 @@ class TestExamAllowanceView(LoggedInTestCase):
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_add_allowance_non_staff_user
(
self
):
# pylint: disable=invalid-name
"""
Test to add allowance with not staff/global user
returns forbidden response.
"""
self
.
user
.
is_staff
=
False
self
.
user
.
save
()
set_runtime_service
(
'instructor'
,
MockInstructorService
(
is_user_course_staff
=
False
))
# Create an exam.
timed_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
,
is_proctored
=
False
)
allowance_data
=
{
'exam_id'
:
timed_exam
.
id
,
'user_info'
:
self
.
student_taking_exam
.
username
,
'key'
:
'a_key'
,
'value'
:
'30'
}
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.allowance'
),
allowance_data
)
self
.
assertEqual
(
response
.
status_code
,
403
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'detail'
],
'Must be a Staff User to Perform this request.'
)
def
test_get_allowances_for_course
(
self
):
def
test_get_allowances_for_course
(
self
):
"""
"""
Remove allowance for a user for an exam.
Remove allowance for a user for an exam.
...
@@ -1688,6 +1909,107 @@ class TestExamAllowanceView(LoggedInTestCase):
...
@@ -1688,6 +1909,107 @@ class TestExamAllowanceView(LoggedInTestCase):
self
.
assertEqual
(
response_data
[
0
][
'proctored_exam'
][
'course_id'
],
proctored_exam
.
course_id
)
self
.
assertEqual
(
response_data
[
0
][
'proctored_exam'
][
'course_id'
],
proctored_exam
.
course_id
)
self
.
assertEqual
(
response_data
[
0
][
'key'
],
allowance_data
[
'key'
])
self
.
assertEqual
(
response_data
[
0
][
'key'
],
allowance_data
[
'key'
])
def
test_get_allowance_non_staff_user
(
self
):
# pylint: disable=invalid-name
"""
Test to get allowance of a user with not staff/global user
returns forbidden response.
"""
self
.
user
.
is_staff
=
False
self
.
user
.
save
()
# 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
,
is_proctored
=
False
)
allowance_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'user_info'
:
self
.
student_taking_exam
.
username
,
'key'
:
'a_key'
,
'value'
:
'30'
}
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.allowance'
),
allowance_data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# update the Instructor Mock service to return the course staff to False
# which will return in the Forbidden request.
set_runtime_service
(
'instructor'
,
MockInstructorService
(
is_user_course_staff
=
False
))
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.allowance'
,
kwargs
=
{
'course_id'
:
proctored_exam
.
course_id
})
)
self
.
assertEqual
(
response
.
status_code
,
403
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'detail'
],
'Must be a Staff User to Perform this request.'
)
def
test_get_timed_exam_allowances_for_course
(
self
):
# pylint: disable=invalid-name
"""
get the timed exam allowances for the course
"""
self
.
user
.
is_staff
=
False
self
.
user
.
save
()
# Create an timed exam.
timed_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
,
is_proctored
=
False
)
allowance_data
=
{
'exam_id'
:
timed_exam
.
id
,
'user_info'
:
self
.
student_taking_exam
.
username
,
'key'
:
'a_key'
,
'value'
:
'30'
}
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.allowance'
),
allowance_data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Create proctored exam.
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content1'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
,
is_proctored
=
True
)
allowance_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'user_info'
:
self
.
student_taking_exam
.
username
,
'key'
:
'a_key'
,
'value'
:
'30'
}
response
=
self
.
client
.
put
(
reverse
(
'edx_proctoring.proctored_exam.allowance'
),
allowance_data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.allowance'
,
kwargs
=
{
'course_id'
:
proctored_exam
.
course_id
})
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
# we should only get the timed exam allowance
# We are not logged in as a global user
self
.
assertEqual
(
len
(
response_data
),
1
)
self
.
assertEqual
(
response_data
[
0
][
'proctored_exam'
][
'course_id'
],
timed_exam
.
course_id
)
self
.
assertEqual
(
response_data
[
0
][
'proctored_exam'
][
'content_id'
],
timed_exam
.
content_id
)
self
.
assertEqual
(
response_data
[
0
][
'key'
],
allowance_data
[
'key'
])
class
TestActiveExamsForUserView
(
LoggedInTestCase
):
class
TestActiveExamsForUserView
(
LoggedInTestCase
):
"""
"""
...
...
edx_proctoring/views.py
View file @
8a1a8152
...
@@ -39,8 +39,9 @@ from edx_proctoring.exceptions import (
...
@@ -39,8 +39,9 @@ from edx_proctoring.exceptions import (
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptDoesNotExistsException
,
ProctoredExamIllegalStatusTransition
,
ProctoredExamIllegalStatusTransition
,
)
)
from
edx_proctoring.runtime
import
get_runtime_service
from
edx_proctoring.serializers
import
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
from
edx_proctoring.serializers
import
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttempt
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttempt
,
ProctoredExam
from
.utils
import
AuthenticatedAPIView
,
get_time_remaining_for_attempt
,
humanized_time
from
.utils
import
AuthenticatedAPIView
,
get_time_remaining_for_attempt
,
humanized_time
...
@@ -64,6 +65,40 @@ def require_staff(func):
...
@@ -64,6 +65,40 @@ def require_staff(func):
return
wrapped
return
wrapped
def
require_course_or_global_staff
(
func
):
"""View decorator that requires that the user have staff permissions. """
def
wrapped
(
request
,
*
args
,
**
kwargs
):
# pylint: disable=missing-docstring
instructor_service
=
get_runtime_service
(
'instructor'
)
course_id
=
kwargs
[
'course_id'
]
if
'course_id'
in
kwargs
else
None
exam_id
=
request
.
DATA
.
get
(
'exam_id'
,
None
)
attempt_id
=
kwargs
[
'attempt_id'
]
if
'attempt_id'
in
kwargs
else
None
if
request
.
user
.
is_staff
:
return
func
(
request
,
*
args
,
**
kwargs
)
else
:
if
course_id
is
None
:
if
exam_id
is
not
None
:
exam
=
ProctoredExam
.
get_exam_by_id
(
exam_id
)
course_id
=
exam
.
course_id
elif
attempt_id
is
not
None
:
exam_attempt
=
ProctoredExamStudentAttempt
.
objects
.
get_exam_attempt_by_id
(
attempt_id
)
course_id
=
exam_attempt
.
proctored_exam
.
course_id
else
:
response_message
=
_
(
"could not determine the course_id"
)
return
Response
(
status
=
status
.
HTTP_403_FORBIDDEN
,
data
=
{
"detail"
:
response_message
}
)
if
instructor_service
.
is_course_staff
(
request
.
user
,
course_id
):
return
func
(
request
,
*
args
,
**
kwargs
)
else
:
return
Response
(
status
=
status
.
HTTP_403_FORBIDDEN
,
data
=
{
"detail"
:
_
(
"Must be a Staff User to Perform this request."
)}
)
return
wrapped
class
ProctoredExamView
(
AuthenticatedAPIView
):
class
ProctoredExamView
(
AuthenticatedAPIView
):
"""
"""
Endpoint for the Proctored Exams
Endpoint for the Proctored Exams
...
@@ -216,8 +251,10 @@ class ProctoredExamView(AuthenticatedAPIView):
...
@@ -216,8 +251,10 @@ class ProctoredExamView(AuthenticatedAPIView):
data
=
{
"detail"
:
"The exam with course_id, content_id does not exist."
}
data
=
{
"detail"
:
"The exam with course_id, content_id does not exist."
}
)
)
else
:
else
:
timed_exams_only
=
not
request
.
user
.
is_staff
result_set
=
get_all_exams_for_course
(
result_set
=
get_all_exams_for_course
(
course_id
=
course_id
course_id
=
course_id
,
timed_exams_only
=
timed_exams_only
)
)
return
Response
(
result_set
)
return
Response
(
result_set
)
...
@@ -383,7 +420,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -383,7 +420,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
data
=
{
"detail"
:
str
(
ex
)}
data
=
{
"detail"
:
str
(
ex
)}
)
)
@method_decorator
(
require_staff
)
@method_decorator
(
require_
course_or_global_
staff
)
def
delete
(
self
,
request
,
attempt_id
):
# pylint: disable=unused-argument
def
delete
(
self
,
request
,
attempt_id
):
# pylint: disable=unused-argument
"""
"""
HTTP DELETE handler. Removes an exam attempt.
HTTP DELETE handler. Removes an exam attempt.
...
@@ -565,17 +602,23 @@ class StudentProctoredExamAttemptsByCourse(AuthenticatedAPIView):
...
@@ -565,17 +602,23 @@ class StudentProctoredExamAttemptsByCourse(AuthenticatedAPIView):
A search parameter is optional
A search parameter is optional
"""
"""
@method_decorator
(
require_staff
)
@method_decorator
(
require_
course_or_global_
staff
)
def
get
(
self
,
request
,
course_id
,
search_by
=
None
):
# pylint: disable=unused-argument
def
get
(
self
,
request
,
course_id
,
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.
"""
"""
# course staff only views attempts of timed exams. edx staff can view both timed and proctored attempts.
time_exams_only
=
not
request
.
user
.
is_staff
if
search_by
is
not
None
:
if
search_by
is
not
None
:
exam_attempts
=
ProctoredExamStudentAttempt
.
objects
.
get_filtered_exam_attempts
(
course_id
,
search_by
)
exam_attempts
=
ProctoredExamStudentAttempt
.
objects
.
get_filtered_exam_attempts
(
course_id
,
search_by
,
time_exams_only
)
attempt_url
=
reverse
(
'edx_proctoring.proctored_exam.attempts.search'
,
args
=
[
course_id
,
search_by
])
attempt_url
=
reverse
(
'edx_proctoring.proctored_exam.attempts.search'
,
args
=
[
course_id
,
search_by
])
else
:
else
:
exam_attempts
=
ProctoredExamStudentAttempt
.
objects
.
get_all_exam_attempts
(
course_id
)
exam_attempts
=
ProctoredExamStudentAttempt
.
objects
.
get_all_exam_attempts
(
course_id
,
time_exams_only
)
attempt_url
=
reverse
(
'edx_proctoring.proctored_exam.attempts.course'
,
args
=
[
course_id
])
attempt_url
=
reverse
(
'edx_proctoring.proctored_exam.attempts.course'
,
args
=
[
course_id
])
paginator
=
Paginator
(
exam_attempts
,
ATTEMPTS_PER_PAGE
)
paginator
=
Paginator
(
exam_attempts
,
ATTEMPTS_PER_PAGE
)
...
@@ -649,17 +692,21 @@ class ExamAllowanceView(AuthenticatedAPIView):
...
@@ -649,17 +692,21 @@ class ExamAllowanceView(AuthenticatedAPIView):
**Response Values**
**Response Values**
* returns Nothing. deletes the allowance for the user proctored exam.
* returns Nothing. deletes the allowance for the user proctored exam.
"""
"""
@method_decorator
(
require_staff
)
@method_decorator
(
require_
course_or_global_
staff
)
def
get
(
self
,
request
,
course_id
):
# pylint: disable=unused-argument
def
get
(
self
,
request
,
course_id
):
# pylint: disable=unused-argument
"""
"""
HTTP GET handler. Get all allowances for a course.
HTTP GET handler. Get all allowances for a course.
"""
"""
# course staff only views attempts of timed exams. edx staff can view both timed and proctored attempts.
time_exams_only
=
not
request
.
user
.
is_staff
result_set
=
get_allowances_for_course
(
result_set
=
get_allowances_for_course
(
course_id
=
course_id
course_id
=
course_id
,
timed_exams_only
=
time_exams_only
)
)
return
Response
(
result_set
)
return
Response
(
result_set
)
@method_decorator
(
require_staff
)
@method_decorator
(
require_
course_or_global_
staff
)
def
put
(
self
,
request
):
def
put
(
self
,
request
):
"""
"""
HTTP GET handler. Adds or updates Allowance
HTTP GET handler. Adds or updates Allowance
...
@@ -679,7 +726,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
...
@@ -679,7 +726,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
data
=
{
"detail"
:
str
(
ex
)}
data
=
{
"detail"
:
str
(
ex
)}
)
)
@method_decorator
(
require_staff
)
@method_decorator
(
require_
course_or_global_
staff
)
def
delete
(
self
,
request
):
def
delete
(
self
,
request
):
"""
"""
HTTP DELETE handler. Removes Allowance.
HTTP DELETE handler. Removes Allowance.
...
...
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