Skip to content

Commit

Permalink
Merge pull request #539 from edx/dcs/remove-attempt
Browse files Browse the repository at this point in the history
Remove exam attempt on backend when it's deleted in the LMS
  • Loading branch information
davestgermain authored Feb 21, 2019
2 parents 936186a + 645a0e3 commit 6e6f579
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ For convenience, the PS should return the exam instructions and the software dow
"download_url": "http://my-proctoring.com/download"
}

``DELETE``: removes attempt on PS server

When an attempt is deleted on the Open edX server, it will make a ``DELETE`` request on the PS server. On success, return::

{
"status": "deleted"
}


User management endpoint
^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
7 changes: 7 additions & 0 deletions edx_proctoring/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ def mark_erroneous_exam_attempt(self, exam, attempt):
"""
raise NotImplementedError()

@abc.abstractmethod
def remove_exam_attempt(self, exam, attempt):
"""
Method that removes the exam attempt from the backend's system
"""
raise NotImplementedError()

@abc.abstractmethod
def get_software_download_url(self):
"""
Expand Down
3 changes: 3 additions & 0 deletions edx_proctoring/backends/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ def mark_erroneous_exam_attempt(self, exam, attempt):
"""
return None

def remove_exam_attempt(self, exam, attempt):
return True

def get_software_download_url(self):
"""
Returns
Expand Down
3 changes: 3 additions & 0 deletions edx_proctoring/backends/null.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def mark_erroneous_exam_attempt(self, exam, attempt):
"""
return None

def remove_exam_attempt(self, exam, attempt):
return True

def get_software_download_url(self):
"""
Returns the URL that the user needs to go to in order to download
Expand Down
10 changes: 10 additions & 0 deletions edx_proctoring/backends/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@ def stop_exam_attempt(self, exam, attempt):
method='PATCH')
return response.get('status')

def remove_exam_attempt(self, exam, attempt):
"""
Removes the exam attempt on the backend provider's server
"""
response = self._make_attempt_request(
exam,
attempt,
method='DELETE')
return response.get('status', None) == 'deleted'

def mark_erroneous_exam_attempt(self, exam, attempt):
"""
Method that is responsible for communicating with the backend provider
Expand Down
3 changes: 3 additions & 0 deletions edx_proctoring/backends/software_secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ def mark_erroneous_exam_attempt(self, exam, attempt):
"""
return None

def remove_exam_attempt(self, exam, attempt):
return None

def get_software_download_url(self):
"""
Returns the URL that the user needs to go to in order to download
Expand Down
14 changes: 14 additions & 0 deletions edx_proctoring/backends/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class TestBackendProvider(ProctoringBackendProvider):

last_retire_user = None
attempt_error = None
last_attempt_remove = None

def register_exam_attempt(self, exam, context):
"""
Expand Down Expand Up @@ -60,6 +61,10 @@ def mark_erroneous_exam_attempt(self, exam, attempt):
"""
return None

def remove_exam_attempt(self, exam, attempt):
self.last_attempt_remove = (exam, attempt)
return True

def get_software_download_url(self):
"""
Returns the URL that the user needs to go to in order to download
Expand Down Expand Up @@ -144,6 +149,12 @@ def mark_erroneous_exam_attempt(self, exam, attempt):
attempt
)

def remove_exam_attempt(self, exam, attempt):
return super(PassthroughBackendProvider, self).remove_exam_attempt(
exam,
attempt
)

def get_software_download_url(self):
"""
Returns the URL that the user needs to go to in order to download
Expand Down Expand Up @@ -191,6 +202,9 @@ def test_raises_exception(self):
with self.assertRaises(NotImplementedError):
provider.mark_erroneous_exam_attempt(None, None)

with self.assertRaises(NotImplementedError):
provider.remove_exam_attempt(None, None)

with self.assertRaises(NotImplementedError):
provider.on_exam_saved(None)

Expand Down
11 changes: 11 additions & 0 deletions edx_proctoring/backends/tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ def test_update_exam_attempt_status(self, provider_method_name, corresponding_st
status = getattr(self.provider, provider_method_name)(self.backend_exam['external_id'], attempt_id)
self.assertEqual(status, corresponding_status)

@responses.activate
def test_remove_attempt(self):
attempt_id = 2
responses.add(
responses.DELETE,
url=self.provider.exam_attempt_url.format(exam_id=self.backend_exam['external_id'], attempt_id=attempt_id),
json={'status': "deleted"}
)
status = self.provider.remove_exam_attempt(self.backend_exam['external_id'], attempt_id)
self.assertTrue(status)

def test_on_review_callback(self):
"""
on_review_callback should just return the payload
Expand Down
12 changes: 12 additions & 0 deletions edx_proctoring/signals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"edx-proctoring signals"
import logging

from django.db.models.signals import pre_save, post_save, pre_delete
from django.dispatch import receiver

Expand All @@ -9,6 +11,8 @@
from edx_proctoring.utils import emit_event, locate_attempt_by_attempt_code
from edx_proctoring.backends import get_backend_provider

log = logging.getLogger(__name__)


@receiver(pre_save, sender=models.ProctoredExam)
def check_for_category_switch(sender, instance, **kwargs): # pylint: disable=unused-argument
Expand Down Expand Up @@ -106,6 +110,14 @@ def on_attempt_changed(sender, instance, signal, **kwargs): # pylint: disable=u
return
else:
return
else:
# remove the attempt on the backend
# timed exams have no backend
backend = get_backend_provider(name=instance.proctored_exam.backend)
if backend:
result = backend.remove_exam_attempt(instance.proctored_exam.external_id, instance.external_id)
if not result:
log.error('Failed to remove attempt %d from %s', instance.id, backend.verbose_name)
models.archive_model(models.ProctoredExamStudentAttemptHistory, instance, id='attempt_id')


Expand Down
7 changes: 7 additions & 0 deletions edx_proctoring/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,14 @@ def test_remove_exam_attempt(self):

proctored_exam_student_attempt = self._create_unstarted_exam_attempt()
remove_exam_attempt(proctored_exam_student_attempt.id, requesting_user=self.user)
test_backend = get_backend_provider(name='test')

self.assertEqual(
test_backend.last_attempt_remove, (
proctored_exam_student_attempt.proctored_exam.external_id,
proctored_exam_student_attempt.external_id
)
)
with self.assertRaises(StudentExamAttemptDoesNotExistsException):
remove_exam_attempt(proctored_exam_student_attempt.id, requesting_user=self.user)

Expand Down

0 comments on commit 6e6f579

Please sign in to comment.