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 (
from edx_proctoring.models import (
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,
......@@ -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
import pytz
from datetime import datetime
from django.db import models
from django.db.models import Q
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from model_utils.models import TimeStampedModel
......@@ -122,6 +123,17 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
exam_attempt_obj = None
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):
"""
......@@ -175,6 +187,13 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
return student_allowance
@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):
"""
Add or (Update) an allowance for a user within a given exam
......
"""Defines serializers used by the Proctoring API."""
from rest_framework import serializers
from edx_proctoring.models import ProctoredExam
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAttempt, ProctoredExamStudentAllowance
class ProctoredExamSerializer(serializers.ModelSerializer):
......@@ -16,3 +16,32 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
"course_id", "content_id", "external_id", "exam_name",
"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
from datetime import datetime
import pytz
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, \
StudentExamAttemptAlreadyExistsException, StudentExamAttemptDoesNotExistsException
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt
......@@ -32,6 +32,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.key = 'Test Key'
self.value = 'Test Value'
self.external_id = 'test_external_id'
self.proctored_exam_id = self._create_proctored_exam()
def _create_proctored_exam(self):
"""
......@@ -48,10 +49,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
Creates the ProctoredExamStudentAttempt object.
"""
proctored_exam_id = self._create_proctored_exam()
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=proctored_exam_id,
proctored_exam_id=self.proctored_exam_id,
user_id=self.user_id,
external_id=self.external_id,
started_at=datetime.now(pytz.UTC)
......@@ -61,32 +60,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
creates allowance for user.
"""
proctored_exam_id = self._create_proctored_exam()
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):
"""
Test to create a proctored exam that has already exist in the
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):
self._create_proctored_exam()
......@@ -94,15 +76,14 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
test update the existing proctored exam
"""
proctored_exam_id = self._create_proctored_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
)
# only those fields were updated, whose
# 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)
......@@ -117,15 +98,14 @@ class ProctoredExamApiTests(LoggedInTestCase):
which will throw the exception
"""
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):
"""
test to get the exam by the exam_id and
then compare their values.
"""
proctored_exam_id = self._create_proctored_exam()
proctored_exam = get_exam_by_id(proctored_exam_id)
proctored_exam = get_exam_by_id(self.proctored_exam_id)
self.assertEqual(proctored_exam['course_id'], self.course_id)
self.assertEqual(proctored_exam['content_id'], self.content_id)
self.assertEqual(proctored_exam['exam_name'], self.exam_name)
......@@ -142,7 +122,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
with self.assertRaises(ProctoredExamNotFoundException):
get_exam_by_id(1)
get_exam_by_id(2)
with self.assertRaises(ProctoredExamNotFoundException):
get_exam_by_content_id('teasd', 'tewasda')
......@@ -151,11 +131,10 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
Test to add allowance for user.
"""
proctored_exam_id = self._create_proctored_exam()
add_allowance_for_user(proctored_exam_id, self.user_id, self.key, self.value)
add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value)
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)
......@@ -176,10 +155,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
Test to get an allowance which does not exist.
"""
proctored_exam_id = self._create_proctored_exam()
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)
......@@ -196,8 +173,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
Start an exam attempt.
"""
proctored_exam_id = self._create_proctored_exam()
attempt_id = start_exam_attempt(proctored_exam_id, self.user_id, self.external_id)
attempt_id = start_exam_attempt(self.proctored_exam_id, self.user_id, self.external_id)
self.assertGreater(attempt_id, 0)
def test_restart_exam_attempt(self):
......@@ -224,6 +200,30 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
Stop an exam attempt that had not started yet.
"""
proctored_exam = self._create_proctored_exam()
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
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):
"""
Endpoint for the Proctored Exams
......@@ -80,6 +93,7 @@ class ProctoredExamView(AuthenticatedAPIView):
?course_id=edX/DemoX/Demo_Course&content_id=123
returns an existing exam object matching the course_id and the content_id
"""
@require_staff
def post(self, request):
"""
Http POST handler. Creates an exam.
......@@ -100,6 +114,7 @@ class ProctoredExamView(AuthenticatedAPIView):
data=serializer.errors
)
@require_staff
def put(self, request):
"""
HTTP PUT handler. To update an exam.
......@@ -186,6 +201,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
status=status.HTTP_200_OK
)
@require_staff
def post(self, request):
"""
HTTP POST handler. To start an exam.
......@@ -204,6 +220,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
data={"detail": "Error. Trying to start an exam that has already started."}
)
@require_staff
def put(self, request):
"""
HTTP POST handler. To stop an exam.
......@@ -231,6 +248,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
HTTP PUT: Creates or Updates the allowance for a user.
HTTP DELETE: Removed an allowance for a user.
"""
@require_staff
def put(self, request):
"""
HTTP GET handler. Adds or updates Allowance
......@@ -242,6 +260,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
value=request.DATA.get('value', "")
))
@require_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