-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: management command for triggering a status update to a proctore…
…d exam attempt through the attempt review As a result of https://github.com/edx/edx-platform/pull/28559, the signal and handler connection in edx-proctoring was broken. As a result, all incoming reviews for proctored exam attempts were unable to correctly update the status of the attempt. This management command is needed so that we can rerun the post_save signal handler for proctored exam attempt reviews, so that the status for those attempts can be updated properly.
- Loading branch information
Showing
5 changed files
with
193 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
edx_proctoring/management/commands/tests/test_update_attempt_status_from_review.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
""" | ||
Tests for the update_attempt_status_from_review management command | ||
""" | ||
from datetime import datetime, timedelta | ||
|
||
import pytz | ||
|
||
from django.core.management import call_command | ||
|
||
from edx_proctoring.api import create_exam, create_exam_attempt, get_exam_attempt_by_id | ||
from edx_proctoring.models import ProctoredExamSoftwareSecureReview | ||
from edx_proctoring.runtime import set_runtime_service | ||
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus, SoftwareSecureReviewStatus | ||
from edx_proctoring.tests.test_services import MockCertificateService, MockCreditService, MockGradesService | ||
from edx_proctoring.tests.utils import LoggedInTestCase | ||
|
||
|
||
class SetAttemptActiveFieldTests(LoggedInTestCase): | ||
""" | ||
Coverage of the update_attempt_status_from_review.py file | ||
""" | ||
|
||
def setUp(self): | ||
""" | ||
Build up test data | ||
""" | ||
super().setUp() | ||
set_runtime_service('credit', MockCreditService()) | ||
set_runtime_service('grades', MockGradesService()) | ||
set_runtime_service('certificates', MockCertificateService()) | ||
self.first_exam_id = create_exam( | ||
course_id='foo', | ||
content_id='bar', | ||
exam_name='Test Exam 1', | ||
time_limit_mins=90 | ||
) | ||
self.second_exam_id = create_exam( | ||
course_id='foo', | ||
content_id='baz', | ||
exam_name='Test Exam 2', | ||
time_limit_mins=90 | ||
) | ||
|
||
# create a first attempt and review | ||
self.first_attempt_id = create_exam_attempt( | ||
self.first_exam_id, | ||
self.user.id, | ||
taking_as_proctored=True | ||
) | ||
self.first_attempt = get_exam_attempt_by_id(self.first_attempt_id) | ||
self.first_attempt_review_object = ProctoredExamSoftwareSecureReview.objects.create( | ||
attempt_code=self.first_attempt['attempt_code'], | ||
exam_id=self.first_exam_id, | ||
student_id=self.user.id, | ||
) | ||
|
||
# create a second attempt and review | ||
self.second_attempt_id = create_exam_attempt( | ||
self.second_exam_id, | ||
self.user.id, | ||
taking_as_proctored=True | ||
) | ||
self.second_attempt = get_exam_attempt_by_id(self.second_attempt_id) | ||
self.second_attempt_review_object = ProctoredExamSoftwareSecureReview.objects.create( | ||
attempt_code=self.second_attempt['attempt_code'], | ||
exam_id=self.second_exam_id, | ||
student_id=self.user.id, | ||
) | ||
|
||
# update both reviews, and use update instead of save to avoid triggering a post_save signal | ||
ProctoredExamSoftwareSecureReview.objects.filter(id=self.first_attempt_review_object.id).update( | ||
review_status=SoftwareSecureReviewStatus.clean | ||
) | ||
ProctoredExamSoftwareSecureReview.objects.filter(id=self.second_attempt_review_object.id).update( | ||
review_status=SoftwareSecureReviewStatus.suspicious | ||
) | ||
|
||
def test_run_command(self): | ||
""" | ||
Run the management command | ||
""" | ||
# check status of attempts | ||
self.assertEqual(self.first_attempt['status'], ProctoredExamStudentAttemptStatus.created) | ||
self.assertEqual(self.second_attempt['status'], ProctoredExamStudentAttemptStatus.created) | ||
# check status of reviews | ||
first_review = ProctoredExamSoftwareSecureReview.objects.get(id=self.first_attempt_review_object.id) | ||
self.assertEqual(first_review.review_status, SoftwareSecureReviewStatus.clean) | ||
second_review = ProctoredExamSoftwareSecureReview.objects.get(id=self.second_attempt_review_object.id) | ||
self.assertEqual(second_review.review_status, SoftwareSecureReviewStatus.suspicious) | ||
|
||
start_time = datetime.now(pytz.UTC) - timedelta(minutes=60) | ||
end_time = datetime.now(pytz.UTC) + timedelta(minutes=60) | ||
|
||
# run command | ||
call_command( | ||
'update_attempt_status_from_review', | ||
batch_size=2, | ||
sleep_time=0, | ||
start_date_time=start_time.strftime('%Y-%m-%d %H:%M:%S'), | ||
end_date_time=end_time.strftime('%Y-%m-%d %H:%M:%S') | ||
) | ||
|
||
# check that attempt statuses has been updated | ||
updated_first_attempt = get_exam_attempt_by_id(self.first_attempt_id) | ||
self.assertEqual(updated_first_attempt['status'], ProctoredExamStudentAttemptStatus.verified) | ||
updated_second_attempt = get_exam_attempt_by_id(self.second_attempt_id) | ||
self.assertEqual(updated_second_attempt['status'], ProctoredExamStudentAttemptStatus.second_review_required) |
80 changes: 80 additions & 0 deletions
80
edx_proctoring/management/commands/update_attempt_status_from_review.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
""" | ||
Django management command to update the status of a ProctoredExamStudentAttempt | ||
based on the status of its corresponding ProctoredExamSoftwareSecureReview | ||
""" | ||
import datetime | ||
import logging | ||
import time | ||
|
||
from django.core.management.base import BaseCommand | ||
|
||
from edx_proctoring.models import ProctoredExamSoftwareSecureReview | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Django Management command to update is_attempt_active field on review models | ||
""" | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument( | ||
'--start_date_time', | ||
action='store', | ||
dest='start_date_time', | ||
type=str, | ||
help='First date time for reviews that we want to consider. Should be formatted as 2020-12-02 00:00:00.' | ||
) | ||
parser.add_argument( | ||
'--end_date_time', | ||
action='store', | ||
dest='end_date_time', | ||
type=str, | ||
help='Last date time for reviews that we want to consider. Should be formatted as 2020-12-02 00:00:00.' | ||
) | ||
parser.add_argument( | ||
'--batch_size', | ||
action='store', | ||
dest='batch_size', | ||
type=int, | ||
default=300, | ||
help='Maximum number of attempt_codes to process. ' | ||
'This helps avoid overloading the database while updating large amount of data.' | ||
) | ||
parser.add_argument( | ||
'--sleep_time', | ||
action='store', | ||
dest='sleep_time', | ||
type=int, | ||
default=10, | ||
help='Sleep time in seconds between update of batches' | ||
) | ||
|
||
def handle(self, *args, **options): | ||
""" | ||
Management command entry point, simply call into the signal firing | ||
""" | ||
batch_size = options['batch_size'] | ||
sleep_time = options['sleep_time'] | ||
review_start_date = datetime.datetime.strptime(options['start_date_time'], '%Y-%m-%d %H:%M:%S') | ||
review_end_date = datetime.datetime.strptime(options['end_date_time'], '%Y-%m-%d %H:%M:%S') | ||
|
||
reviews_in_date_range = ProctoredExamSoftwareSecureReview.objects.filter( | ||
modified__range=[review_start_date, review_end_date] | ||
) | ||
review_count = 0 | ||
|
||
for review in reviews_in_date_range: | ||
review_id = review.id | ||
attempt_code = review.attempt_code | ||
log.info('Saving review_id={review_id} for corresponding attempt_code={attempt_code}'.format( | ||
review_id=review_id, | ||
attempt_code=attempt_code | ||
)) | ||
review.save() | ||
review_count += 1 | ||
|
||
if review_count == batch_size: | ||
review_count = 0 | ||
time.sleep(sleep_time) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters