Commit 54591dd5 by Muhammad Shoaib

PHX-38 added the api level functionality

parent e577b8a6
...@@ -6,6 +6,14 @@ ...@@ -6,6 +6,14 @@
In-Proc API (aka Library) for the edx_proctoring subsystem. This is not to be confused with a HTTP REST In-Proc API (aka Library) for the edx_proctoring subsystem. This is not to be confused with a HTTP REST
API which is in the views.py file, per edX coding standards API which is in the views.py file, per edX coding standards
""" """
import pytz
from datetime import datetime
from edx_proctoring.exceptions import (
ProctoredExamAlreadyExist, ProctoredExamNotFoundException, StudentExamAttemptAlreadyExistException
)
from edx_proctoring.models import (
ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt
)
def create_exam(course_id, content_id, exam_name, time_limit_mins, def create_exam(course_id, content_id, exam_name, time_limit_mins,
...@@ -16,6 +24,19 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins, ...@@ -16,6 +24,19 @@ 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:
raise ProctoredExamAlreadyExist
proctored_exam = ProctoredExam.objects.create(
course_id=course_id,
content_id=content_id,
external_id=external_id,
exam_name=exam_name,
time_limit_mins=time_limit_mins,
is_proctored=is_proctored,
is_active=is_active
)
return proctored_exam
def update_exam(exam_id, exam_name=None, time_limit_mins=None, def update_exam(exam_id, exam_name=None, time_limit_mins=None,
...@@ -26,6 +47,22 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, ...@@ -26,6 +47,22 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None,
Returns: id Returns: id
""" """
proctored_exam = ProctoredExam.get_exam_by_id(exam_id)
if proctored_exam is None:
raise ProctoredExamNotFoundException
if exam_name is not None:
proctored_exam.exam_name = exam_name
if time_limit_mins is not None:
proctored_exam.time_limit_mins = time_limit_mins
if is_proctored is not None:
proctored_exam.is_proctored = is_proctored
if external_id is not None:
proctored_exam.external_id = external_id
if is_active is not None:
proctored_exam.is_active = is_active
proctored_exam.save()
return proctored_exam
def get_exam_by_id(exam_id): def get_exam_by_id(exam_id):
...@@ -34,6 +71,11 @@ def get_exam_by_id(exam_id): ...@@ -34,6 +71,11 @@ def get_exam_by_id(exam_id):
Returns dictionary version of the Django ORM object Returns dictionary version of the Django ORM object
""" """
proctored_exam = ProctoredExam.get_exam_by_id(exam_id)
if proctored_exam is None:
raise ProctoredExamNotFoundException
return proctored_exam.__dict__
def get_exam_by_content_id(course_id, content_id): def get_exam_by_content_id(course_id, content_id):
...@@ -42,18 +84,27 @@ def get_exam_by_content_id(course_id, content_id): ...@@ -42,18 +84,27 @@ def get_exam_by_content_id(course_id, content_id):
Returns dictionary version of the Django ORM object Returns dictionary version of the Django ORM object
""" """
proctored_exam = ProctoredExam.get_exam_by_content_id(course_id, content_id)
if proctored_exam is None:
raise ProctoredExamNotFoundException
return proctored_exam.__dict__
def add_allowance_for_user(exam_id, user_id, key, value): def add_allowance_for_user(exam_id, user_id, key, value):
""" """
Adds (or updates) an allowance for a user within a given exam Adds (or updates) an allowance for a user within a given exam
""" """
ProctoredExamStudentAllowance.add_allowance_for_user(exam_id, user_id, key, value)
def remove_allowance_for_user(exam_id, user_id, key): def remove_allowance_for_user(exam_id, user_id, key):
""" """
Deletes an allowance for a user within a given exam. Deletes an allowance for a user within a given exam.
""" """
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(exam_id, user_id, key)
if student_allowance is not None:
student_allowance.delete()
def start_exam_attempt(exam_id, user_id, external_id): def start_exam_attempt(exam_id, user_id, external_id):
...@@ -63,12 +114,24 @@ def start_exam_attempt(exam_id, user_id, external_id): ...@@ -63,12 +114,24 @@ def start_exam_attempt(exam_id, user_id, external_id):
Returns: exam_attempt_id (PK) Returns: exam_attempt_id (PK)
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.start_exam_attempt(exam_id, user_id, external_id)
if exam_attempt_obj is None:
raise StudentExamAttemptAlreadyExistException
else:
return exam_attempt_obj
def stop_exam_attempt(exam_id, user): def stop_exam_attempt(exam_id, user_id):
""" """
Marks the exam attempt as completed (sets the completed_at field and updates the record) Marks the exam attempt as completed (sets the completed_at field and updates the record)
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.get_student_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None:
raise StudentExamAttemptAlreadyExistException
else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
exam_attempt_obj.save()
return exam_attempt_obj
def get_active_exams_for_user(user_id, course_id=None): def get_active_exams_for_user(user_id, course_id=None):
......
"""
Specialized exceptions for the Notification subsystem
"""
class ProctoredExamAlreadyExist(Exception):
"""
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
"""
class ProctoredExamNotFoundException(Exception):
"""
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
"""
class StudentExamAttemptAlreadyExistException(Exception):
"""
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
"""
""" """
Data models for the proctoring subsystem Data models for the proctoring subsystem
""" """
import pytz
from datetime import datetime
from django.db import models from django.db import models
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
...@@ -37,6 +39,30 @@ class ProctoredExam(TimeStampedModel): ...@@ -37,6 +39,30 @@ class ProctoredExam(TimeStampedModel):
""" Meta class for this Django model """ """ Meta class for this Django model """
unique_together = (('course_id', 'content_id'),) unique_together = (('course_id', 'content_id'),)
@classmethod
def get_exam_by_content_id(cls, course_id, content_id):
"""
Returns the Proctored Exam if found else returns None,
Given course_id and content_id
"""
try:
proctored_exam = cls.objects.get(course_id=course_id, content_id=content_id)
except cls.DoesNotExist:
proctored_exam = None
return proctored_exam
@classmethod
def get_exam_by_id(cls, exam_id):
"""
Returns the Proctored Exam if found else returns None,
Given exam_id (PK)
"""
try:
proctored_exam = cls.objects.get(id=exam_id)
except cls.DoesNotExist:
proctored_exam = None
return proctored_exam
class ProctoredExamStudentAttempt(TimeStampedModel): class ProctoredExamStudentAttempt(TimeStampedModel):
""" """
...@@ -62,6 +88,34 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -62,6 +88,34 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
""" returns boolean if this attempt is considered active """ """ returns boolean if this attempt is considered active """
return self.started_at and not self.completed_at return self.started_at and not self.completed_at
@classmethod
def start_exam_attempt(cls, exam_id, user_id, external_id):
"""
create and return an exam attempt entry for a given
exam_id. If one already exists, then returns None.
"""
if cls.get_student_exam_attempt(exam_id, user_id) is None:
return cls.objects.create(
proctored_exam=exam_id,
user_id=user_id,
external_id=external_id,
started_at=datetime.now(pytz.UTC)
)
else:
return None
@classmethod
def get_student_exam_attempt(cls, exam_id, user_id):
"""
Returns the Student Exam Attempt object if found
else Returns None.
"""
try:
exam_attempt_obj = cls.objects.get(proctored_exam=exam_id, user_id=user_id)
except cls.DoesNotExist:
exam_attempt_obj = None
return exam_attempt_obj
class QuerySetWithUpdateOverride(models.query.QuerySet): class QuerySetWithUpdateOverride(models.query.QuerySet):
""" """
...@@ -101,6 +155,29 @@ class ProctoredExamStudentAllowance(TimeStampedModel): ...@@ -101,6 +155,29 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
""" Meta class for this Django model """ """ Meta class for this Django model """
unique_together = (('user_id', 'proctored_exam', 'key'),) unique_together = (('user_id', 'proctored_exam', 'key'),)
@classmethod
def get_allowance_for_user(cls, exam_id, user_id, key):
"""
Returns an allowance for a user within a given exam
"""
try:
student_allowance = cls.objects.get(proctored_exam=exam_id, user_id=user_id, key=key)
except cls.DoesNotExist:
student_allowance = None
return student_allowance
@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
"""
try:
student_allowance = cls.objects.get(proctored_exam=exam_id, user_id=user_id, key=key)
student_allowance.value = value
student_allowance.save()
except cls.DoesNotExist:
cls.objects.create(proctored_exam=exam_id, user_id=user_id, key=key, value=value)
class ProctoredExamStudentAllowanceHistory(TimeStampedModel): class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
""" """
......
"""
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
from edx_proctoring.exceptions import ProctoredExamAlreadyExist, ProctoredExamNotFoundException, \
StudentExamAttemptAlreadyExistException
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAllowanceHistory, \
ProctoredExamStudentAttempt
from .utils import (
LoggedInTestCase
)
class ProctoredExamApiTests(LoggedInTestCase):
"""
All tests for the models.py
"""
def setUp(self):
"""
Build out test harnessing
"""
super(ProctoredExamApiTests, self).setUp()
self.default_time_limit = 21
self.course_id = 'test_course'
self.content_id = 'test_content_id'
self.exam_name = 'Test Exam'
self.user_id = 1
self.key = 'Test Key'
self.value = 'Test Value'
self.external_id = 'test_external_id'
def _create_proctored_exam(self):
return create_exam(
course_id=self.course_id,
content_id=self.content_id,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit
)
def _create_student_exam_attempt_entry(self):
proctored_exam = self._create_proctored_exam()
return ProctoredExamStudentAttempt.objects.create(
proctored_exam=proctored_exam,
user_id=self.user_id,
external_id=self.external_id,
started_at=datetime.now(pytz.UTC)
)
def _add_allowance_for_user(self):
proctored_exam = self._create_proctored_exam()
return ProctoredExamStudentAllowance.objects.create(
proctored_exam=proctored_exam, 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_already_existing_exam_throws_exception(self):
"""
Test to create a proctored exam that has already exist in the
database and will throw an exception ProctoredExamAlreadyExist.
"""
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(ProctoredExamAlreadyExist):
self._create_proctored_exam()
def test_update_proctored_exam(self):
"""
test update the existing proctored exam
"""
proctored_exam = self._create_proctored_exam()
update_proctored_exam = update_exam(
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(update_proctored_exam.exam_name, 'Updated Exam Name')
self.assertEqual(update_proctored_exam.time_limit_mins, 30)
self.assertEqual(update_proctored_exam.course_id, 'test_course')
self.assertEqual(update_proctored_exam.content_id, 'test_content_id')
def test_update_non_existing_proctored_exam(self):
"""
test to update the non-existing proctored exam
which will throw the exception
"""
with self.assertRaises(ProctoredExamNotFoundException):
update_exam(1, 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 = self._create_proctored_exam()
proctored_exam = get_exam_by_id(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)
proctored_exam = get_exam_by_content_id(self.course_id, self.content_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)
def test_get_invalid_proctored_exam(self):
"""
test to get the exam by the invalid exam_id which will
raises exception
"""
with self.assertRaises(ProctoredExamNotFoundException):
get_exam_by_id(1)
with self.assertRaises(ProctoredExamNotFoundException):
get_exam_by_content_id('teasd', 'tewasda')
def test_add_allowance_for_user(self):
proctored_exam = self._create_proctored_exam()
add_allowance_for_user(proctored_exam, self.user_id, self.key, self.value)
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
proctored_exam.id, self.user_id, self.key
)
self.assertIsNotNone(student_allowance)
def test_allowance_for_user_already_exists(self):
student_allowance = self._add_allowance_for_user()
add_allowance_for_user(student_allowance.proctored_exam, self.user_id, self.key, 'new_value')
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
student_allowance.proctored_exam.id, self.user_id, self.key
)
self.assertIsNotNone(student_allowance)
self.assertEqual(student_allowance.value, 'new_value')
def test_get_allowance_for_user_does_not_exist(self):
proctored_exam = self._create_proctored_exam()
student_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
proctored_exam.id, self.user_id, self.key
)
self.assertIsNone(student_allowance)
def test_remove_allowance_for_user(self):
student_allowance = self._add_allowance_for_user()
self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 1)
remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key)
self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 0)
def test_student_exam_attempt_entry_already_exists(self):
proctored_exam = self._create_proctored_exam()
start_exam_attempt(proctored_exam, self.user_id, self.external_id)
self.assertIsNotNone(start_exam_attempt)
def test_create_student_exam_attempt_entry(self):
proctored_exam_student_attempt = self._create_student_exam_attempt_entry()
with self.assertRaises(StudentExamAttemptAlreadyExistException):
start_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id, self.external_id)
def test_stop_exam_attempt(self):
proctored_exam_student_attempt = self._create_student_exam_attempt_entry()
self.assertIsNone(proctored_exam_student_attempt.completed_at)
proctored_exam_student_attempt = stop_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id)
self.assertIsNotNone(proctored_exam_student_attempt.completed_at)
def test_stop_invalid_exam_attempt_raises_exception(self):
proctored_exam = self._create_proctored_exam()
with self.assertRaises(StudentExamAttemptAlreadyExistException):
stop_exam_attempt(proctored_exam, self.user_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