Skip to content

Commit

Permalink
Merge pull request #500 from edx/dcs/update-to-timed
Browse files Browse the repository at this point in the history
EDUCATOR-3857 -- switching from proctored to timed
  • Loading branch information
davestgermain committed Jan 4, 2019
2 parents f238f04 + 255470c commit cdab034
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 206 deletions.
26 changes: 0 additions & 26 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
from django.utils.translation import ugettext as _, ugettext_noop
from django.conf import settings
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save
from django.template import loader
from django.urls import reverse, NoReverseMatch
from django.core.mail.message import EmailMessage
Expand Down Expand Up @@ -201,30 +199,6 @@ def remove_review_policy(exam_id):
exam_review_policy.delete()


@receiver(post_save, sender=ProctoredExamReviewPolicy)
@receiver(post_save, sender=ProctoredExam)
def _save_exam_on_backend(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Save the exam to the backend provider when our model changes.
It also combines the review policy into the exam when saving to the backend
"""
if sender == ProctoredExam:
exam_obj = instance
review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(instance.id)
else:
exam_obj = instance.proctored_exam
review_policy = instance
if exam_obj.is_proctored:
exam = ProctoredExamSerializer(exam_obj).data
if review_policy:
exam['rule_summary'] = review_policy.review_policy
backend = get_backend_provider(exam)
external_id = backend.on_exam_saved(exam)
if external_id and external_id != exam_obj.external_id:
exam_obj.external_id = external_id
exam_obj.save()


def get_review_policy_by_exam_id(exam_id):
"""
Looks up exam by the Primary Key. Raises exception if not found.
Expand Down
1 change: 1 addition & 0 deletions edx_proctoring/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def ready(self):
"""
Loads the available proctoring backends
"""
from edx_proctoring import signals # pylint: disable=unused-variable
config = settings.PROCTORING_BACKENDS

self.backends = {} # pylint: disable=W0201
Expand Down
9 changes: 5 additions & 4 deletions edx_proctoring/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
from django.apps import apps


def get_backend_provider(exam=None):
def get_backend_provider(exam=None, name=None):
"""
Returns an instance of the configured backend provider
Passing in an exam will return the backend for that exam
Passing in a name will return the named backend
"""
backend_name = None
if exam:
if 'is_proctored' in exam and not exam['is_proctored']:
# timed exams don't have a backend
return None
elif exam['backend']:
backend_name = exam['backend']
return apps.get_app_config('edx_proctoring').get_backend(name=backend_name)
name = exam['backend']
return apps.get_app_config('edx_proctoring').get_backend(name=name)
2 changes: 2 additions & 0 deletions edx_proctoring/backends/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ def test_get_different_backend(self):
"""
backend = get_backend_provider({'backend': 'null'})
self.assertIsInstance(backend, NullBackendProvider)
backend = get_backend_provider(name='test')
self.assertIsInstance(backend, TestBackendProvider)

def test_backend_choices(self):
"""
Expand Down
189 changes: 13 additions & 176 deletions edx_proctoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
from django.db import models
from django.db.models import Q
from django.db.models.base import ObjectDoesNotExist
from django.db.models.signals import pre_save, pre_delete
from django.dispatch import receiver
from django.utils.translation import ugettext_noop

from model_utils.models import TimeStampedModel
Expand Down Expand Up @@ -187,44 +185,6 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
raise NotImplementedError()


# Hook up the pre_save signal to record creations in the ProctoredExamReviewPolicyHistory table.
@receiver(pre_save, sender=ProctoredExamReviewPolicy)
def on_review_policy_saved(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created.
"""

if instance.id:
# only for update cases
original = ProctoredExamReviewPolicy.objects.get(id=instance.id)
_make_review_policy_archive_copy(original)


# Hook up the pre_delete signal to record creations in the ProctoredExamReviewPolicyHistory table.
@receiver(pre_delete, sender=ProctoredExamReviewPolicy)
def on_review_policy_deleted(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archive the allowance when the item is about to be deleted
"""

_make_review_policy_archive_copy(instance)


def _make_review_policy_archive_copy(instance):
"""
Do the copying into the history table
"""

archive_object = ProctoredExamReviewPolicyHistory(
original_id=instance.id,
set_by_user_id=instance.set_by_user_id,
proctored_exam=instance.proctored_exam,
review_policy=instance.review_policy,
)
archive_object.save()


class ProctoredExamStudentAttemptManager(models.Manager):
"""
Custom manager
Expand Down Expand Up @@ -473,67 +433,20 @@ class Meta:
verbose_name = 'proctored exam attempt history'


@receiver(pre_delete, sender=ProctoredExamStudentAttempt)
def on_attempt_deleted(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archive the exam attempt when the item is about to be deleted
Make a clone and populate in the History table
"""

archive_object = ProctoredExamStudentAttemptHistory(
user=instance.user,
attempt_id=instance.id,
proctored_exam=instance.proctored_exam,
started_at=instance.started_at,
completed_at=instance.completed_at,
attempt_code=instance.attempt_code,
external_id=instance.external_id,
allowed_time_limit_mins=instance.allowed_time_limit_mins,
status=instance.status,
taking_as_proctored=instance.taking_as_proctored,
is_sample_attempt=instance.is_sample_attempt,
student_name=instance.student_name,
review_policy_id=instance.review_policy_id,
last_poll_timestamp=instance.last_poll_timestamp,
last_poll_ipaddr=instance.last_poll_ipaddr,

)
archive_object.save()


@receiver(pre_save, sender=ProctoredExamStudentAttempt)
def on_attempt_updated(sender, instance, **kwargs): # pylint: disable=unused-argument
def archive_model(model, instance, **mapping):
"""
Archive the exam attempt whenever the attempt status is about to be
modified. Make a new entry with the previous value of the status in the
ProctoredExamStudentAttemptHistory table.
Archives the instance to the given history model
optionally maps field names from the instance model to the history model
"""

if instance.id:
# on an update case, get the original
# and see if the status has changed, if so, then we need
# to archive it
original = ProctoredExamStudentAttempt.objects.get(id=instance.id)

if original.status != instance.status:
archive_object = ProctoredExamStudentAttemptHistory(
user=original.user,
attempt_id=original.id,
proctored_exam=original.proctored_exam,
started_at=original.started_at,
completed_at=original.completed_at,
attempt_code=original.attempt_code,
external_id=original.external_id,
allowed_time_limit_mins=original.allowed_time_limit_mins,
status=original.status,
taking_as_proctored=original.taking_as_proctored,
is_sample_attempt=original.is_sample_attempt,
student_name=original.student_name,
review_policy_id=original.review_policy_id,
last_poll_timestamp=original.last_poll_timestamp,
last_poll_ipaddr=original.last_poll_ipaddr,
)
archive_object.save()
archive = model()
# timestampedmodels automatically create these
mapping['created'] = mapping['modified'] = None
for field in instance._meta.get_fields():
to_name = mapping.get(field.name, field.name)
if to_name is not None:
setattr(archive, to_name, getattr(instance, field.name, None))
archive.save()
return archive


class QuerySetWithUpdateOverride(models.QuerySet):
Expand All @@ -543,7 +456,7 @@ class QuerySetWithUpdateOverride(models.QuerySet):
"""
def update(self, **kwargs):
super(QuerySetWithUpdateOverride, self).update(**kwargs)
_make_archive_copy(self.get())
archive_model(ProctoredExamStudentAllowanceHistory, self.get(), id='allowance_id')


class ProctoredExamStudentAllowanceManager(models.Manager):
Expand Down Expand Up @@ -717,43 +630,6 @@ class Meta:
verbose_name = 'proctored allowance history'


# Hook up the post_save signal to record creations in the ProctoredExamStudentAllowanceHistory table.
@receiver(pre_save, sender=ProctoredExamStudentAllowance)
def on_allowance_saved(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created.
"""

if instance.id:
original = ProctoredExamStudentAllowance.objects.get(id=instance.id)
_make_archive_copy(original)


@receiver(pre_delete, sender=ProctoredExamStudentAllowance)
def on_allowance_deleted(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archive the allowance when the item is about to be deleted
"""

_make_archive_copy(instance)


def _make_archive_copy(item):
"""
Make a clone and populate in the History table
"""

archive_object = ProctoredExamStudentAllowanceHistory(
allowance_id=item.id,
user=item.user,
proctored_exam=item.proctored_exam,
key=item.key,
value=item.value
)
archive_object.save()


class ProctoredExamSoftwareSecureReview(TimeStampedModel):
"""
This is where we store the proctored exam review feedback
Expand Down Expand Up @@ -842,45 +718,6 @@ class Meta:
verbose_name = 'Proctored exam review archive'


# Hook up the post_save signal to record creations in the ProctoredExamStudentAllowanceHistory table.
@receiver(pre_save, sender=ProctoredExamSoftwareSecureReview)
def on_review_saved(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created.
"""

if instance.id:
# only for update cases
original = ProctoredExamSoftwareSecureReview.objects.get(id=instance.id)
_make_review_archive_copy(original)


@receiver(pre_delete, sender=ProctoredExamSoftwareSecureReview)
def on_review_deleted(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archive the allowance when the item is about to be deleted
"""

_make_review_archive_copy(instance)


def _make_review_archive_copy(instance):
"""
Do the copying into the history table
"""

archive_object = ProctoredExamSoftwareSecureReviewHistory(
attempt_code=instance.attempt_code,
review_status=instance.review_status,
raw_data=instance.raw_data,
reviewed_by=instance.reviewed_by,
student=instance.student,
exam=instance.exam,
)
archive_object.save()


class ProctoredExamSoftwareSecureComment(TimeStampedModel):
"""
This is where we store the proctored exam review comments
Expand Down
Loading

0 comments on commit cdab034

Please sign in to comment.