Commit 8a1a8152 by chrisndodge

Merge pull request #162 from edx/muhhshoaib/SOL-1222-update-permissions-in-edx-proctoring

SOL-1222
parents 8aceb2ef 4510a7ca
......@@ -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)
def get_allowances_for_course(course_id):
def get_allowances_for_course(course_id, timed_exams_only):
"""
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]
......@@ -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
of dictionaries, whose schema is the same as what is returned in
......@@ -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]
......
......@@ -83,14 +83,18 @@ class ProctoredExam(TimeStampedModel):
return proctored_exam
@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
"""
result = cls.objects.filter(course_id=course_id)
filtered_query = Q(course_id=course_id)
if active_only:
result = result.filter(is_active=True)
return result
filtered_query = filtered_query & Q(is_active=True)
if timed_exams_only:
filtered_query = filtered_query & Q(is_proctored=False)
return cls.objects.filter(filtered_query)
class ProctoredExamStudentAttemptStatus(object):
......@@ -355,20 +359,26 @@ class ProctoredExamStudentAttemptManager(models.Manager):
exam_attempt_obj = None
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.
"""
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.
"""
filtered_query = Q(proctored_exam__course_id=course_id) & (
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')
......@@ -605,11 +615,15 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
verbose_name = 'proctored allowance'
@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.
"""
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
def get_allowance_for_user(cls, exam_id, user_id, key):
......
......@@ -117,7 +117,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.footer_msg = 'About Proctored Exams'
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):
"""
......@@ -305,9 +305,27 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertEqual(proctored_exam['content_id'], self.content_id)
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)
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):
"""
test to get the exam by the invalid exam_id which will
......@@ -356,7 +374,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
Test to get all the allowances for a course.
"""
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(course_allowances[0]['proctored_exam']['course_id'], allowance.proctored_exam.course_id)
......
......@@ -86,12 +86,24 @@ class MockInstructorService(object):
"""
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
"""
Mock implementation
"""
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):
"""
......
......@@ -39,8 +39,9 @@ from edx_proctoring.exceptions import (
StudentExamAttemptDoesNotExistsException,
ProctoredExamIllegalStatusTransition,
)
from edx_proctoring.runtime import get_runtime_service
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
......@@ -64,6 +65,40 @@ def require_staff(func):
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):
"""
Endpoint for the Proctored Exams
......@@ -216,8 +251,10 @@ class ProctoredExamView(AuthenticatedAPIView):
data={"detail": "The exam with course_id, content_id does not exist."}
)
else:
timed_exams_only = not request.user.is_staff
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)
......@@ -383,7 +420,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
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
"""
HTTP DELETE handler. Removes an exam attempt.
......@@ -565,17 +602,23 @@ class StudentProctoredExamAttemptsByCourse(AuthenticatedAPIView):
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
"""
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:
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])
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])
paginator = Paginator(exam_attempts, ATTEMPTS_PER_PAGE)
......@@ -649,17 +692,21 @@ class ExamAllowanceView(AuthenticatedAPIView):
**Response Values**
* 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
"""
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(
course_id=course_id
course_id=course_id,
timed_exams_only=time_exams_only
)
return Response(result_set)
@method_decorator(require_staff)
@method_decorator(require_course_or_global_staff)
def put(self, request):
"""
HTTP GET handler. Adds or updates Allowance
......@@ -679,7 +726,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
data={"detail": str(ex)}
)
@method_decorator(require_staff)
@method_decorator(require_course_or_global_staff)
def delete(self, request):
"""
HTTP DELETE handler. Removes Allowance.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment