Skip to content

Commit

Permalink
feat: AA-773: Add in logic to mark special exams as complete
Browse files Browse the repository at this point in the history
We have had numerous support tickets/bugs related to special exams
not properly displaying completeness (see https://openedx.atlassian.net/browse/AA-773 for
details). Along with a corresponding edx-platform PR, this will now
call the instructor service to complete all children within a special
exam upon the exam being completed for the first time. Actual completion
happens in the edx-platform PR
  • Loading branch information
Dillon-Dumesnil committed Jun 10, 2021
1 parent 11fdb76 commit f76a428
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Change Log
Unreleased
~~~~~~~~~~

[3.14.0] - 2021-06-10
~~~~~~~~~~~~~~~~~~~~~
* When an exam attempt is finished for the first time, mark all completable children in the exam as complete
in the Completion Service using the Instructor Service. If the Completion Service is not enabled, nothing
will happen.

[3.13.2] - 2021-06-09
~~~~~~~~~~~~~~~~~~~~~
* Extend exam attempt API to return total time left in the attempt
Expand Down
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"""

# Be sure to update the version number in edx_proctoring/package.json
__version__ = '3.13.2'
__version__ = '3.14.0'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
13 changes: 13 additions & 0 deletions edx_proctoring/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from edx_proctoring import api, constants, models
from edx_proctoring.backends import get_backend_provider
from edx_proctoring.runtime import get_runtime_service
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus, SoftwareSecureReviewStatus
from edx_proctoring.utils import emit_event, locate_attempt_by_attempt_code

Expand Down Expand Up @@ -105,6 +106,18 @@ def on_attempt_changed(sender, instance, signal, **kwargs): # pylint: disable=u
# to archive it
original = sender.objects.get(id=instance.id)

# if the exam was finished for the first time, we want to mark it as
# complete in the Completion Service.
# This functionality was added because regardless of submission status
# on individual problems, we want to mark the entire exam as complete
# when the exam is finished since there are no more actions a learner can take.
if not original.completed_at and instance.completed_at:
instructor_service = get_runtime_service('instructor')
if instructor_service:
username = instance.user.username
content_id = instance.proctored_exam.content_id
instructor_service.complete_student_attempt(username, content_id)

if original.status != instance.status:
instance = original
else:
Expand Down
6 changes: 6 additions & 0 deletions edx_proctoring/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ def delete_student_attempt(self, student_identifier, course_id, content_id, requ
raise UserNotFoundException
return True

def complete_student_attempt(self, user_identifier, content_id):
"""
Mock implementation
"""
return True

def is_course_staff(self, user, course_id):
"""
Mocked implementation of is_course_staff
Expand Down
44 changes: 33 additions & 11 deletions edx_proctoring/tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
"""
Tests for signals.py
"""

from unittest.mock import patch

import ddt
from httmock import HTTMock

from django.db.models.signals import pre_delete
from django.db.models.signals import pre_delete, pre_save

from edx_proctoring.api import update_attempt_status
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAttempt
from edx_proctoring.signals import on_attempt_changed
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
from edx_proctoring.tests.test_services import MockInstructorService

from .utils import ProctoredExamTestCase


@ddt.ddt
class SignalTests(ProctoredExamTestCase):
"""
Tests for signals.py
Expand All @@ -22,26 +26,44 @@ def setUp(self):
super().setUp()
self.backend_name = 'software_secure'
self.proctored_exam = ProctoredExam.objects.create(
course_id='x/y/z', content_id=self.content_id, exam_name=self.exam_name,
time_limit_mins=self.default_time_limit, external_id=self.external_id,
backend=self.backend_name
course_id='x/y/z', content_id=self.content_id, exam_name=self.exam_name,
time_limit_mins=self.default_time_limit, external_id=self.external_id,
backend=self.backend_name
)
self.attempt = ProctoredExamStudentAttempt.objects.create(
proctored_exam=self.proctored_exam, user=self.user, attempt_code='12345',
external_id='abcde'
)

pre_delete.connect(on_attempt_changed, sender=ProctoredExamStudentAttempt)
pre_save.connect(on_attempt_changed, sender=ProctoredExamStudentAttempt)

def tearDown(self):
super().tearDown()
pre_delete.disconnect()
pre_save.disconnect()

@patch('logging.Logger.error')
def test_backend_fails_to_delete_attempt(self, logger_mock):
attempt = ProctoredExamStudentAttempt.objects.create(
proctored_exam=self.proctored_exam, user=self.user, attempt_code='12345',
external_id='abcde'
)

# If there is no response from the backend, assert that it is logged correctly
with HTTMock(None):
attempt.delete_exam_attempt()
self.attempt.delete_exam_attempt()
log_format_string = ('Failed to remove attempt_id=%s from backend=%s')
logger_mock.assert_any_call(log_format_string, 1, self.backend_name)

@ddt.data(None, MockInstructorService())
@patch('edx_proctoring.signals.get_runtime_service')
@patch('edx_proctoring.tests.test_services.MockInstructorService.complete_student_attempt')
def test_pre_save_complete_student_attempt(
self, runtime_return, mock_complete_student_attempt, mock_get_runtime_service
):
"""
Ensures the complete_student_attempt function will not be called if the instructor service
is not found.
"""
mock_get_runtime_service.return_value = runtime_return
update_attempt_status(self.attempt.id, ProctoredExamStudentAttemptStatus.submitted)
if runtime_return:
mock_complete_student_attempt.assert_called_once_with(self.user.username, self.content_id)
else:
mock_complete_student_attempt.assert_not_called()
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@edx/edx-proctoring",
"//": "Be sure to update the version number in edx_proctoring/__init__.py",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
"version": "3.13.2",
"version": "3.14.0",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"
Expand Down

0 comments on commit f76a428

Please sign in to comment.