Commit 17f93b7d by Afzal Wali

Added Serializers.

Removed Query Parameters and added them to the URL regex.
parent cdc4d215
...@@ -9,11 +9,12 @@ API which is in the views.py file, per edX coding standards ...@@ -9,11 +9,12 @@ API which is in the views.py file, per edX coding standards
import pytz import pytz
from datetime import datetime from datetime import datetime
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExist, ProctoredExamNotFoundException, StudentExamAttemptAlreadyExistException ProctoredExamAlreadyExists, ProctoredExamNotFoundException, StudentExamAttemptAlreadyExistsException
) )
from edx_proctoring.models import ( from edx_proctoring.models import (
ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt
) )
from edx_proctoring.serializers import ProctoredExamSerializer
def create_exam(course_id, content_id, exam_name, time_limit_mins, def create_exam(course_id, content_id, exam_name, time_limit_mins,
...@@ -25,7 +26,7 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins, ...@@ -25,7 +26,7 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins,
Returns: id (PK) Returns: id (PK)
""" """
if ProctoredExam.get_exam_by_content_id(course_id, content_id) is not None: if ProctoredExam.get_exam_by_content_id(course_id, content_id) is not None:
raise ProctoredExamAlreadyExist raise ProctoredExamAlreadyExists
proctored_exam = ProctoredExam.objects.create( proctored_exam = ProctoredExam.objects.create(
course_id=course_id, course_id=course_id,
...@@ -70,12 +71,23 @@ def get_exam_by_id(exam_id): ...@@ -70,12 +71,23 @@ def get_exam_by_id(exam_id):
Looks up exam by the Primary Key. Raises exception if not found. Looks up exam by the Primary Key. Raises exception if not found.
Returns dictionary version of the Django ORM object Returns dictionary version of the Django ORM object
e.g.
{
"course_id": "edX/DemoX/Demo_Course",
"content_id": "123",
"external_id": "",
"exam_name": "Midterm",
"time_limit_mins": 90,
"is_proctored": true,
"is_active": true
}
""" """
proctored_exam = ProctoredExam.get_exam_by_id(exam_id) proctored_exam = ProctoredExam.get_exam_by_id(exam_id)
if proctored_exam is None: if proctored_exam is None:
raise ProctoredExamNotFoundException raise ProctoredExamNotFoundException
return proctored_exam.__dict__ serialized_exam_object = ProctoredExamSerializer(proctored_exam)
return serialized_exam_object.data
def get_exam_by_content_id(course_id, content_id): def get_exam_by_content_id(course_id, content_id):
...@@ -83,12 +95,23 @@ def get_exam_by_content_id(course_id, content_id): ...@@ -83,12 +95,23 @@ def get_exam_by_content_id(course_id, content_id):
Looks up exam by the course_id/content_id pair. Raises exception if not found. Looks up exam by the course_id/content_id pair. Raises exception if not found.
Returns dictionary version of the Django ORM object Returns dictionary version of the Django ORM object
e.g.
{
"course_id": "edX/DemoX/Demo_Course",
"content_id": "123",
"external_id": "",
"exam_name": "Midterm",
"time_limit_mins": 90,
"is_proctored": true,
"is_active": true
}
""" """
proctored_exam = ProctoredExam.get_exam_by_content_id(course_id, content_id) proctored_exam = ProctoredExam.get_exam_by_content_id(course_id, content_id)
if proctored_exam is None: if proctored_exam is None:
raise ProctoredExamNotFoundException raise ProctoredExamNotFoundException
return proctored_exam.__dict__ serialized_exam_object = ProctoredExamSerializer(proctored_exam)
return serialized_exam_object.data
def add_allowance_for_user(exam_id, user_id, key, value): def add_allowance_for_user(exam_id, user_id, key, value):
...@@ -116,7 +139,7 @@ def start_exam_attempt(exam_id, user_id, external_id): ...@@ -116,7 +139,7 @@ def start_exam_attempt(exam_id, user_id, external_id):
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.start_exam_attempt(exam_id, user_id, external_id) exam_attempt_obj = ProctoredExamStudentAttempt.start_exam_attempt(exam_id, user_id, external_id)
if exam_attempt_obj is None: if exam_attempt_obj is None:
raise StudentExamAttemptAlreadyExistException raise StudentExamAttemptAlreadyExistsException
else: else:
return exam_attempt_obj.id return exam_attempt_obj.id
...@@ -127,7 +150,7 @@ def stop_exam_attempt(exam_id, user_id): ...@@ -127,7 +150,7 @@ def stop_exam_attempt(exam_id, user_id):
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.get_student_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.get_student_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None: if exam_attempt_obj is None:
raise StudentExamAttemptAlreadyExistException raise StudentExamAttemptAlreadyExistsException
else: else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC) exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
exam_attempt_obj.save() exam_attempt_obj.save()
......
...@@ -3,7 +3,7 @@ Specialized exceptions for the Notification subsystem ...@@ -3,7 +3,7 @@ Specialized exceptions for the Notification subsystem
""" """
class ProctoredExamAlreadyExist(Exception): class ProctoredExamAlreadyExists(Exception):
""" """
Generic exception when a look up fails. Since we are abstracting away the backends Generic exception when a look up fails. Since we are abstracting away the backends
we need to catch any native exceptions and re-throw as a generic exception we need to catch any native exceptions and re-throw as a generic exception
...@@ -17,7 +17,7 @@ class ProctoredExamNotFoundException(Exception): ...@@ -17,7 +17,7 @@ class ProctoredExamNotFoundException(Exception):
""" """
class StudentExamAttemptAlreadyExistException(Exception): class StudentExamAttemptAlreadyExistsException(Exception):
""" """
Generic exception when a look up fails. Since we are abstracting away the backends Generic exception when a look up fails. Since we are abstracting away the backends
we need to catch any native exceptions and re-throw as a generic exception we need to catch any native exceptions and re-throw as a generic exception
......
"""Defines serializers used by the Proctoring API."""
from rest_framework import serializers
from edx_proctoring.models import ProctoredExam
class ProctoredExamSerializer(serializers.ModelSerializer):
class Meta:
model = ProctoredExam
fields = (
"course_id", "content_id", "external_id", "exam_name",
"time_limit_mins", "is_proctored", "is_active"
)
...@@ -5,8 +5,8 @@ from datetime import datetime ...@@ -5,8 +5,8 @@ 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, add_allowance_for_user, \ 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 remove_allowance_for_user, start_exam_attempt, stop_exam_attempt
from edx_proctoring.exceptions import ProctoredExamAlreadyExist, ProctoredExamNotFoundException, \ from edx_proctoring.exceptions import ProctoredExamAlreadyExists, ProctoredExamNotFoundException, \
StudentExamAttemptAlreadyExistException StudentExamAttemptAlreadyExistsException
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAllowanceHistory, \ from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAllowanceHistory, \
ProctoredExamStudentAttempt ProctoredExamStudentAttempt
...@@ -78,7 +78,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -78,7 +78,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
is_proctored=True, is_proctored=True,
is_active=True is_active=True
) )
with self.assertRaises(ProctoredExamAlreadyExist): with self.assertRaises(ProctoredExamAlreadyExists):
self._create_proctored_exam() self._create_proctored_exam()
def test_update_proctored_exam(self): def test_update_proctored_exam(self):
...@@ -178,7 +178,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -178,7 +178,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
def test_create_student_exam_attempt_entry(self): def test_create_student_exam_attempt_entry(self):
proctored_exam_student_attempt = self._create_student_exam_attempt_entry() proctored_exam_student_attempt = self._create_student_exam_attempt_entry()
with self.assertRaises(StudentExamAttemptAlreadyExistException): with self.assertRaises(StudentExamAttemptAlreadyExistsException):
start_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id, self.external_id) start_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id, self.external_id)
def test_stop_exam_attempt(self): def test_stop_exam_attempt(self):
...@@ -191,5 +191,5 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -191,5 +191,5 @@ class ProctoredExamApiTests(LoggedInTestCase):
def test_stop_invalid_exam_attempt_raises_exception(self): def test_stop_invalid_exam_attempt_raises_exception(self):
proctored_exam = self._create_proctored_exam() proctored_exam = self._create_proctored_exam()
with self.assertRaises(StudentExamAttemptAlreadyExistException): with self.assertRaises(StudentExamAttemptAlreadyExistsException):
stop_exam_attempt(proctored_exam, self.user_id) stop_exam_attempt(proctored_exam, self.user_id)
...@@ -2,13 +2,24 @@ ...@@ -2,13 +2,24 @@
URL mappings for edX Proctoring Server. URL mappings for edX Proctoring Server.
""" """
from edx_proctoring import views from edx_proctoring import views
from django.conf import settings
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
urlpatterns = patterns( # pylint: disable=invalid-name urlpatterns = patterns( # pylint: disable=invalid-name
'', '',
url( url(
r'edx_proctoring/v1/proctored_exam/exam', r'edx_proctoring/v1/proctored_exam/exam$',
views.ProctoredExamView.as_view(),
name='edx_proctoring.proctored_exam.exam'
),
url(
r'edx_proctoring/v1/proctored_exam/exam/exam_id/(?P<exam_id>\d+)$',
views.ProctoredExamView.as_view(),
name='edx_proctoring.proctored_exam.exam'
),
url(
r'edx_proctoring/v1/proctored_exam/exam/course_id/{}/content_id/(?P<content_id>\d+)$'.format(settings.COURSE_ID_PATTERN),
views.ProctoredExamView.as_view(), views.ProctoredExamView.as_view(),
name='edx_proctoring.proctored_exam.exam' name='edx_proctoring.proctored_exam.exam'
), ),
...@@ -18,12 +29,12 @@ urlpatterns = patterns( # pylint: disable=invalid-name ...@@ -18,12 +29,12 @@ urlpatterns = patterns( # pylint: disable=invalid-name
name='edx_proctoring.proctored_exam.attempt' name='edx_proctoring.proctored_exam.attempt'
), ),
url( url(
r'edx_proctoring/v1/proctored_exam/allowance', r'edx_proctoring/v1/proctored_exam/allowance$',
views.ExamAllowanceView.as_view(), views.ExamAllowanceView.as_view(),
name='edx_proctoring.proctored_exam.allowance' name='edx_proctoring.proctored_exam.allowance'
), ),
url( url(
r'edx_proctoring/v1/proctored_exam/active_exams_for_user', r'edx_proctoring/v1/proctored_exam/active_exams_for_user$',
views.ActiveExamsForUserView.as_view(), views.ActiveExamsForUserView.as_view(),
name='edx_proctoring.proctored_exam.active_exams_for_user' name='edx_proctoring.proctored_exam.active_exams_for_user'
), ),
......
...@@ -3,12 +3,12 @@ Proctored Exams HTTP-based API endpoints ...@@ -3,12 +3,12 @@ Proctored Exams HTTP-based API endpoints
""" """
import logging import logging
from django.db import IntegrityError
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from edx_proctoring.api import create_exam, update_exam, get_exam_by_id, get_exam_by_content_id, start_exam_attempt, \ from edx_proctoring.api import create_exam, update_exam, get_exam_by_id, get_exam_by_content_id, start_exam_attempt, \
stop_exam_attempt, add_allowance_for_user, remove_allowance_for_user, get_active_exams_for_user stop_exam_attempt, add_allowance_for_user, remove_allowance_for_user, get_active_exams_for_user
from edx_proctoring.exceptions import ProctoredExamNotFoundException, ProctoredExamAlreadyExists
from edx_proctoring.serializers import ProctoredExamSerializer
from .utils import AuthenticatedAPIView from .utils import AuthenticatedAPIView
...@@ -31,10 +31,10 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -31,10 +31,10 @@ class ProctoredExamView(AuthenticatedAPIView):
"course_id": "edX/DemoX/Demo_Course", "course_id": "edX/DemoX/Demo_Course",
"content_id": 123, "content_id": 123,
"exam_name": "Midterm", "exam_name": "Midterm",
"time_limit_mins": "90", "time_limit_mins": 90,
"is_proctored": true, "is_proctored": true,
"external_id": "", "external_id": "",
"is_active": true, "is_active": true
} }
**POST data Parameters** **POST data Parameters**
...@@ -79,25 +79,25 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -79,25 +79,25 @@ 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
""" """
def post(self, request): def post(self, request):
""" """
Http POST handler. Creates an exam. Http POST handler. Creates an exam.
""" """
try: serializer = ProctoredExamSerializer(data=request.DATA)
exam_id = create_exam( if serializer.is_valid():
course_id=request.DATA.get('course_id', ""), try:
content_id=request.DATA.get('content_id', ""), exam_id = create_exam(**request.DATA)
exam_name=request.DATA.get('exam_name', ""), return Response({'exam_id': exam_id})
time_limit_mins=request.DATA.get('time_limit_mins', ""), except ProctoredExamAlreadyExists:
is_proctored=True if request.DATA.get('is_proctored', "False").lower() == 'true' else False, return Response(
external_id=request.DATA.get('external_id', ""), status=status.HTTP_400_BAD_REQUEST,
is_active=True if request.DATA.get('is_active', "").lower() == 'true' else False, data={"detail": "Error Trying to create a duplicate exam."}
) )
return Response({'exam_id': exam_id}) else:
except IntegrityError:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "Trying to create a duplicate exam."} data=serializer.errors
) )
def put(self, request): def put(self, request):
...@@ -110,9 +110,9 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -110,9 +110,9 @@ class ProctoredExamView(AuthenticatedAPIView):
exam_id=request.DATA.get('exam_id', ""), exam_id=request.DATA.get('exam_id', ""),
exam_name=request.DATA.get('exam_name', ""), exam_name=request.DATA.get('exam_name', ""),
time_limit_mins=request.DATA.get('time_limit_mins', ""), time_limit_mins=request.DATA.get('time_limit_mins', ""),
is_proctored=True if request.DATA.get('is_proctored', "False").lower() == 'true' else False, is_proctored=request.DATA.get('is_proctored', False),
external_id=request.DATA.get('external_id', ""), external_id=request.DATA.get('external_id', ""),
is_active=True if request.DATA.get('is_active', "").lower() == 'true' else False, is_active=request.DATA.get('is_active', False),
) )
return Response({'exam_id': exam_id}) return Response({'exam_id': exam_id})
except: except:
...@@ -121,7 +121,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -121,7 +121,7 @@ class ProctoredExamView(AuthenticatedAPIView):
data={"detail": "The exam_id does not exist."} data={"detail": "The exam_id does not exist."}
) )
def get(self, request): def get(self, request, exam_id=None, course_id=None, content_id=None):
""" """
HTTP GET handler. HTTP GET handler.
Scenarios: Scenarios:
...@@ -130,17 +130,13 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -130,17 +130,13 @@ class ProctoredExamView(AuthenticatedAPIView):
""" """
course_id = request.QUERY_PARAMS.get('course_id', None)
content_id = request.QUERY_PARAMS.get('content_id', None)
exam_id = request.QUERY_PARAMS.get('exam_id', None)
if exam_id: if exam_id:
try: try:
return Response( return Response(
data=get_exam_by_id(exam_id), data=get_exam_by_id(exam_id),
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )
except Exception: except ProctoredExamNotFoundException:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam_id does not exist."} data={"detail": "The exam_id does not exist."}
...@@ -151,7 +147,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -151,7 +147,7 @@ class ProctoredExamView(AuthenticatedAPIView):
data=get_exam_by_content_id(course_id, content_id), data=get_exam_by_content_id(course_id, content_id),
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )
except Exception: except ProctoredExamNotFoundException:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam with course_id, content_id does not exist."} data={"detail": "The exam with course_id, content_id does not exist."}
......
...@@ -67,3 +67,5 @@ MIDDLEWARE_CLASSES = ( ...@@ -67,3 +67,5 @@ MIDDLEWARE_CLASSES = (
) )
ROOT_URLCONF = 'edx_proctoring.urls' ROOT_URLCONF = 'edx_proctoring.urls'
COURSE_ID_PATTERN = r'(?P<course_id>[^/+]+(/|\+)[^/+]+(/|\+)[^/]+)'
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