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
edx
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
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
119 additions
and
26 deletions
+119
-26
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
+0
-0
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
This diff is collapsed.
Click to expand it.
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