Commit bf15198d by Muhammad Shoaib

added the decorator for the staff level permission and the added the api…

added the decorator for the staff level permission and the added the api endpoint for get all the active time exams.
parent 9602bc1c
...@@ -14,7 +14,8 @@ from edx_proctoring.exceptions import ( ...@@ -14,7 +14,8 @@ from edx_proctoring.exceptions import (
from edx_proctoring.models import ( from edx_proctoring.models import (
ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt
) )
from edx_proctoring.serializers import ProctoredExamSerializer from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer, \
ProctoredExamStudentAllowanceSerializer
def create_exam(course_id, content_id, exam_name, time_limit_mins, def create_exam(course_id, content_id, exam_name, time_limit_mins,
...@@ -176,3 +177,22 @@ def get_active_exams_for_user(user_id, course_id=None): ...@@ -176,3 +177,22 @@ def get_active_exams_for_user(user_id, course_id=None):
}, {}, ...] }, {}, ...]
""" """
result = []
student_active_exams = ProctoredExamStudentAttempt.get_active_student_exams(user_id, course_id)
for active_exam in student_active_exams:
# convert the django orm objects
# into the serialized form.
exam_serialized_data = ProctoredExamSerializer(active_exam.proctored_exam).data
active_exam_serialized_data = ProctoredExamStudentAttemptSerializer(active_exam).data
student_allowances = ProctoredExamStudentAllowance.get_allowances_for_user(
active_exam.proctored_exam.id, user_id
)
allowance_serialized_data = [ProctoredExamStudentAllowanceSerializer(allowance).data for allowance in
student_allowances]
result.append({
'exam': exam_serialized_data,
'attempt': active_exam_serialized_data,
'allowances': allowance_serialized_data
})
return result
...@@ -4,6 +4,7 @@ Data models for the proctoring subsystem ...@@ -4,6 +4,7 @@ Data models for the proctoring subsystem
import pytz import pytz
from datetime import datetime from datetime import datetime
from django.db import models from django.db import models
from django.db.models import Q
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -122,6 +123,17 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -122,6 +123,17 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
exam_attempt_obj = None exam_attempt_obj = None
return exam_attempt_obj return exam_attempt_obj
@classmethod
def get_active_student_exams(cls, 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 cls.objects.filter(filtered_query)
class QuerySetWithUpdateOverride(models.query.QuerySet): class QuerySetWithUpdateOverride(models.query.QuerySet):
""" """
...@@ -175,6 +187,13 @@ class ProctoredExamStudentAllowance(TimeStampedModel): ...@@ -175,6 +187,13 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
return student_allowance return student_allowance
@classmethod @classmethod
def get_allowances_for_user(cls, exam_id, user_id):
"""
Returns an allowances for a user within a given exam
"""
return cls.objects.filter(proctored_exam_id=exam_id, user_id=user_id)
@classmethod
def add_allowance_for_user(cls, exam_id, user_id, key, value): def add_allowance_for_user(cls, exam_id, user_id, key, value):
""" """
Add or (Update) an allowance for a user within a given exam Add or (Update) an allowance for a user within a given exam
......
"""Defines serializers used by the Proctoring API.""" """Defines serializers used by the Proctoring API."""
from rest_framework import serializers from rest_framework import serializers
from edx_proctoring.models import ProctoredExam from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAttempt, ProctoredExamStudentAllowance
class ProctoredExamSerializer(serializers.ModelSerializer): class ProctoredExamSerializer(serializers.ModelSerializer):
...@@ -16,3 +16,32 @@ class ProctoredExamSerializer(serializers.ModelSerializer): ...@@ -16,3 +16,32 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
"course_id", "content_id", "external_id", "exam_name", "course_id", "content_id", "external_id", "exam_name",
"time_limit_mins", "is_proctored", "is_active" "time_limit_mins", "is_proctored", "is_active"
) )
class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
"""
Serializer for the ProctoredExamStudentAttempt Model.
"""
class Meta:
"""
Meta Class
"""
model = ProctoredExamStudentAttempt
fields = (
"created", "modified", "user_id", "started_at", "completed_at",
"external_id", "status"
)
class ProctoredExamStudentAllowanceSerializer(serializers.ModelSerializer):
"""
Serializer for the ProctoredExamStudentAllowance Model.
"""
class Meta:
"""
Meta Class
"""
model = ProctoredExamStudentAllowance
fields = (
"created", "modified", "user_id", "key", "value"
)
...@@ -4,7 +4,7 @@ All tests for the models.py ...@@ -4,7 +4,7 @@ All tests for the models.py
from datetime import datetime from datetime import datetime
import pytz import pytz
from edx_proctoring.api import create_exam, update_exam, get_exam_by_id, get_exam_by_content_id, \ from edx_proctoring.api import create_exam, update_exam, get_exam_by_id, get_exam_by_content_id, \
add_allowance_for_user, remove_allowance_for_user, start_exam_attempt, stop_exam_attempt add_allowance_for_user, remove_allowance_for_user, start_exam_attempt, stop_exam_attempt, get_active_exams_for_user
from edx_proctoring.exceptions import ProctoredExamAlreadyExists, ProctoredExamNotFoundException, \ from edx_proctoring.exceptions import ProctoredExamAlreadyExists, ProctoredExamNotFoundException, \
StudentExamAttemptAlreadyExistsException, StudentExamAttemptDoesNotExistsException StudentExamAttemptAlreadyExistsException, StudentExamAttemptDoesNotExistsException
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt
...@@ -32,6 +32,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -32,6 +32,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.key = 'Test Key' self.key = 'Test Key'
self.value = 'Test Value' self.value = 'Test Value'
self.external_id = 'test_external_id' self.external_id = 'test_external_id'
self.proctored_exam_id = self._create_proctored_exam()
def _create_proctored_exam(self): def _create_proctored_exam(self):
""" """
...@@ -48,10 +49,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -48,10 +49,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
Creates the ProctoredExamStudentAttempt object. Creates the ProctoredExamStudentAttempt object.
""" """
proctored_exam_id = self._create_proctored_exam()
return ProctoredExamStudentAttempt.objects.create( return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=proctored_exam_id, proctored_exam_id=self.proctored_exam_id,
user_id=self.user_id, user_id=self.user_id,
external_id=self.external_id, external_id=self.external_id,
started_at=datetime.now(pytz.UTC) started_at=datetime.now(pytz.UTC)
...@@ -61,32 +60,15 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -61,32 +60,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
creates allowance for user. creates allowance for user.
""" """
proctored_exam_id = self._create_proctored_exam()
return ProctoredExamStudentAllowance.objects.create( return ProctoredExamStudentAllowance.objects.create(
proctored_exam_id=proctored_exam_id, user_id=self.user_id, key=self.key, value=self.value proctored_exam_id=self.proctored_exam_id, user_id=self.user_id, key=self.key, value=self.value
) )
def test_create_exam(self):
"""
Test to create a proctored exam.
"""
proctored_exam = self._create_proctored_exam()
self.assertIsNotNone(proctored_exam)
def test_create_duplicate_exam(self): def test_create_duplicate_exam(self):
""" """
Test to create a proctored exam that has already exist in the Test to create a proctored exam that has already exist in the
database and will throw an exception ProctoredExamAlreadyExists. database and will throw an exception ProctoredExamAlreadyExists.
""" """
ProctoredExam.objects.create(
course_id='test_course',
content_id='test_content_id',
external_id='test_external_id',
exam_name='Test Exam',
time_limit_mins=21,
is_proctored=True,
is_active=True
)
with self.assertRaises(ProctoredExamAlreadyExists): with self.assertRaises(ProctoredExamAlreadyExists):
self._create_proctored_exam() self._create_proctored_exam()
...@@ -94,15 +76,14 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -94,15 +76,14 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
test update the existing proctored exam test update the existing proctored exam
""" """
proctored_exam_id = self._create_proctored_exam()
updated_proctored_exam_id = update_exam( updated_proctored_exam_id = update_exam(
proctored_exam_id, exam_name='Updated Exam Name', time_limit_mins=30, self.proctored_exam_id, exam_name='Updated Exam Name', time_limit_mins=30,
is_proctored=True, external_id='external_id', is_active=True is_proctored=True, external_id='external_id', is_active=True
) )
# only those fields were updated, whose # only those fields were updated, whose
# values are passed. # values are passed.
self.assertEqual(proctored_exam_id, updated_proctored_exam_id) self.assertEqual(self.proctored_exam_id, updated_proctored_exam_id)
update_proctored_exam = ProctoredExam.objects.get(id=updated_proctored_exam_id) update_proctored_exam = ProctoredExam.objects.get(id=updated_proctored_exam_id)
...@@ -117,15 +98,14 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -117,15 +98,14 @@ class ProctoredExamApiTests(LoggedInTestCase):
which will throw the exception which will throw the exception
""" """
with self.assertRaises(ProctoredExamNotFoundException): with self.assertRaises(ProctoredExamNotFoundException):
update_exam(1, exam_name='Updated Exam Name', time_limit_mins=30) update_exam(2, exam_name='Updated Exam Name', time_limit_mins=30)
def test_get_proctored_exam(self): def test_get_proctored_exam(self):
""" """
test to get the exam by the exam_id and test to get the exam by the exam_id and
then compare their values. then compare their values.
""" """
proctored_exam_id = self._create_proctored_exam() proctored_exam = get_exam_by_id(self.proctored_exam_id)
proctored_exam = get_exam_by_id(proctored_exam_id)
self.assertEqual(proctored_exam['course_id'], self.course_id) self.assertEqual(proctored_exam['course_id'], self.course_id)
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)
...@@ -142,7 +122,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -142,7 +122,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
with self.assertRaises(ProctoredExamNotFoundException): with self.assertRaises(ProctoredExamNotFoundException):
get_exam_by_id(1) get_exam_by_id(2)
with self.assertRaises(ProctoredExamNotFoundException): with self.assertRaises(ProctoredExamNotFoundException):
get_exam_by_content_id('teasd', 'tewasda') get_exam_by_content_id('teasd', 'tewasda')
...@@ -151,11 +131,10 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -151,11 +131,10 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
Test to add allowance for user. Test to add allowance for user.
""" """
proctored_exam_id = self._create_proctored_exam() add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value)
add_allowance_for_user(proctored_exam_id, self.user_id, self.key, self.value)
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user( student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
proctored_exam_id, self.user_id, self.key self.proctored_exam_id, self.user_id, self.key
) )
self.assertIsNotNone(student_allowance) self.assertIsNotNone(student_allowance)
...@@ -176,10 +155,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -176,10 +155,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
Test to get an allowance which does not exist. Test to get an allowance which does not exist.
""" """
proctored_exam_id = self._create_proctored_exam()
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user( student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
proctored_exam_id, self.user_id, self.key self.proctored_exam_id, self.user_id, self.key
) )
self.assertIsNone(student_allowance) self.assertIsNone(student_allowance)
...@@ -196,8 +173,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -196,8 +173,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
Start an exam attempt. Start an exam attempt.
""" """
proctored_exam_id = self._create_proctored_exam() attempt_id = start_exam_attempt(self.proctored_exam_id, self.user_id, self.external_id)
attempt_id = start_exam_attempt(proctored_exam_id, self.user_id, self.external_id)
self.assertGreater(attempt_id, 0) self.assertGreater(attempt_id, 0)
def test_restart_exam_attempt(self): def test_restart_exam_attempt(self):
...@@ -224,6 +200,30 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -224,6 +200,30 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
Stop an exam attempt that had not started yet. Stop an exam attempt that had not started yet.
""" """
proctored_exam = self._create_proctored_exam()
with self.assertRaises(StudentExamAttemptDoesNotExistsException): with self.assertRaises(StudentExamAttemptDoesNotExistsException):
stop_exam_attempt(proctored_exam, self.user_id) stop_exam_attempt(self.proctored_exam_id, self.user_id)
def test_get_active_exams_for_user(self):
"""
Test to get the all the active
exams for the user.
"""
active_exam_attempt = self._create_student_exam_attempt()
self.assertEqual(active_exam_attempt.is_active, True)
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
)
start_exam_attempt(
exam_id=exam_id,
user_id=self.user_id,
external_id=self.external_id
)
add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value)
add_allowance_for_user(self.proctored_exam_id, self.user_id, 'new_key', 'new_value')
student_active_exams = get_active_exams_for_user(self.user_id, self.course_id)
self.assertEqual(len(student_active_exams), 2)
self.assertEqual(len(student_active_exams[0]['allowances']), 2)
self.assertEqual(len(student_active_exams[1]['allowances']), 0)
...@@ -16,6 +16,19 @@ from .utils import AuthenticatedAPIView ...@@ -16,6 +16,19 @@ from .utils import AuthenticatedAPIView
LOG = logging.getLogger("edx_proctoring_views") LOG = logging.getLogger("edx_proctoring_views")
def require_staff(func):
"""View decorator that requires that the user have staff permissions. """
def wrapped(request, *args, **kwargs): # pylint: disable=missing-docstring
if args[0].user.is_staff:
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
...@@ -80,6 +93,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -80,6 +93,7 @@ class ProctoredExamView(AuthenticatedAPIView):
?course_id=edX/DemoX/Demo_Course&content_id=123 ?course_id=edX/DemoX/Demo_Course&content_id=123
returns an existing exam object matching the course_id and the content_id returns an existing exam object matching the course_id and the content_id
""" """
@require_staff
def post(self, request): def post(self, request):
""" """
Http POST handler. Creates an exam. Http POST handler. Creates an exam.
...@@ -100,6 +114,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -100,6 +114,7 @@ class ProctoredExamView(AuthenticatedAPIView):
data=serializer.errors data=serializer.errors
) )
@require_staff
def put(self, request): def put(self, request):
""" """
HTTP PUT handler. To update an exam. HTTP PUT handler. To update an exam.
...@@ -186,6 +201,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -186,6 +201,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )
@require_staff
def post(self, request): def post(self, request):
""" """
HTTP POST handler. To start an exam. HTTP POST handler. To start an exam.
...@@ -204,6 +220,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -204,6 +220,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
data={"detail": "Error. Trying to start an exam that has already started."} data={"detail": "Error. Trying to start an exam that has already started."}
) )
@require_staff
def put(self, request): def put(self, request):
""" """
HTTP POST handler. To stop an exam. HTTP POST handler. To stop an exam.
...@@ -231,6 +248,7 @@ class ExamAllowanceView(AuthenticatedAPIView): ...@@ -231,6 +248,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
HTTP PUT: Creates or Updates the allowance for a user. HTTP PUT: Creates or Updates the allowance for a user.
HTTP DELETE: Removed an allowance for a user. HTTP DELETE: Removed an allowance for a user.
""" """
@require_staff
def put(self, request): def put(self, request):
""" """
HTTP GET handler. Adds or updates Allowance HTTP GET handler. Adds or updates Allowance
...@@ -242,6 +260,7 @@ class ExamAllowanceView(AuthenticatedAPIView): ...@@ -242,6 +260,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
value=request.DATA.get('value', "") value=request.DATA.get('value', "")
)) ))
@require_staff
def delete(self, request): def delete(self, request):
""" """
HTTP DELETE handler. Removes Allowance. 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