-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #752 from edx/alangsto/management_command
Added management command for updating is_attempt_active field
- Loading branch information
Showing
5 changed files
with
195 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
104 changes: 104 additions & 0 deletions
104
edx_proctoring/management/commands/set_is_attempt_active.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,104 @@ | ||
""" | ||
Django management command to update the is_attempt_active field on | ||
ProctoredExamSoftwareSecureReview and ProctoredExamSoftwareSecureReviewHistory models | ||
""" | ||
import logging | ||
import time | ||
|
||
from django.core.management.base import BaseCommand | ||
|
||
from edx_proctoring.models import ( | ||
ProctoredExamSoftwareSecureReview, | ||
ProctoredExamSoftwareSecureReviewHistory, | ||
ProctoredExamStudentAttempt | ||
) | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Django Management command to update is_attempt_active field on review models | ||
""" | ||
update_field_count = 0 | ||
update_attempt_codes = [] | ||
distinct_attempt_codes = set() | ||
|
||
def add_arguments(self, parser): | ||
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 locking 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'] | ||
|
||
log.info('Updating is_attempt_active field for current reviews.') | ||
for review in ProctoredExamSoftwareSecureReview.objects.filter(is_attempt_active=True): | ||
self.check_and_update(review, batch_size, sleep_time) | ||
|
||
log.info('Updating is_attempt_active field for archived reviews.') | ||
for archived_review in ProctoredExamSoftwareSecureReviewHistory.objects.filter(is_attempt_active=True): | ||
self.check_and_update(archived_review, batch_size, sleep_time, only_update_archives=True) | ||
|
||
if self.update_attempt_codes: | ||
log.info('Updating {} reviews'.format(len(self.update_attempt_codes))) | ||
self.bulk_update(self.update_attempt_codes, False) | ||
|
||
def check_and_update(self, review_object, size, sleep_time, only_update_archives=False): | ||
""" | ||
Function to check if a review object should be updated, and updates accordingly | ||
""" | ||
if review_object.attempt_code not in self.distinct_attempt_codes: | ||
if self.should_update(review_object): | ||
self.distinct_attempt_codes.add(review_object.attempt_code) | ||
self.update_attempt_codes.append(review_object.attempt_code) | ||
self.update_field_count += 1 | ||
log.info('Adding review {} to be updated'.format(review_object.id)) | ||
|
||
if self.update_field_count == size: | ||
log.info('Updating {} reviews'.format(size)) | ||
self.bulk_update(self.update_attempt_codes, only_update_archives) | ||
self.update_field_count = 0 | ||
self.update_attempt_codes = [] | ||
time.sleep(sleep_time) | ||
|
||
def bulk_update(self, attempt_codes, only_update_archive): | ||
""" | ||
Updates the is_attempt_active fields for all reviews who have an attempt code in attempt_codes | ||
""" | ||
if not only_update_archive: | ||
reviews = ProctoredExamSoftwareSecureReview.objects.filter(attempt_code__in=attempt_codes) | ||
reviews.update(is_attempt_active=False) | ||
|
||
archived_reviews = ProctoredExamSoftwareSecureReviewHistory.objects.filter(attempt_code__in=attempt_codes) | ||
archived_reviews.update(is_attempt_active=False) | ||
|
||
def should_update(self, review_object): | ||
""" | ||
Returns a boolean based on whether an attempt exists in the ProctoredExamStudentAttempt model | ||
""" | ||
attempt_code = review_object.attempt_code | ||
try: | ||
ProctoredExamStudentAttempt.objects.get(attempt_code=attempt_code) | ||
return False | ||
except ProctoredExamStudentAttempt.DoesNotExist: | ||
return True |
85 changes: 85 additions & 0 deletions
85
edx_proctoring/management/commands/tests/test_set_is_attempt_active.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,85 @@ | ||
""" | ||
Tests for the set_is_attempt_active management command | ||
""" | ||
|
||
from django.core.management import call_command | ||
|
||
from edx_proctoring.api import create_exam, create_exam_attempt, get_exam_attempt_by_id, remove_exam_attempt | ||
from edx_proctoring.models import ProctoredExamSoftwareSecureReview, ProctoredExamSoftwareSecureReviewHistory | ||
from edx_proctoring.runtime import set_runtime_service | ||
from edx_proctoring.tests.test_services import MockCertificateService, MockCreditService, MockGradesService | ||
from edx_proctoring.tests.utils import LoggedInTestCase | ||
|
||
|
||
class SetAttemptActiveFieldTests(LoggedInTestCase): | ||
""" | ||
Coverage of the set_attempt_status.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.exam_id = create_exam( | ||
course_id='foo', | ||
content_id='bar', | ||
exam_name='Test Exam', | ||
time_limit_mins=90 | ||
) | ||
|
||
self.attempt_id = create_exam_attempt( | ||
self.exam_id, | ||
self.user.id, | ||
taking_as_proctored=True | ||
) | ||
|
||
self.attempt = get_exam_attempt_by_id(self.attempt_id) | ||
|
||
ProctoredExamSoftwareSecureReview.objects.create( | ||
attempt_code=self.attempt['attempt_code'], | ||
exam_id=self.exam_id, | ||
student_id=self.user.id, | ||
) | ||
|
||
def test_run_command(self): | ||
""" | ||
Run the management command | ||
""" | ||
|
||
# check that review is there | ||
reviews = ProctoredExamSoftwareSecureReview.objects.all() | ||
self.assertEqual(len(reviews), 1) | ||
|
||
archive_reviews = ProctoredExamSoftwareSecureReviewHistory.objects.all() | ||
self.assertEqual(len(archive_reviews), 0) | ||
|
||
# archive attempt | ||
remove_exam_attempt(self.attempt_id, requesting_user=self.user) | ||
|
||
# check that field is false | ||
review = ProctoredExamSoftwareSecureReview.objects.get(attempt_code=self.attempt['attempt_code']) | ||
self.assertFalse(review.is_attempt_active) | ||
|
||
# change field back to true for testing | ||
review.is_attempt_active = True | ||
review.save() | ||
|
||
# expect there to be two archived reviews, one from removing the attempt, and one because we changed a field | ||
archive_reviews = ProctoredExamSoftwareSecureReviewHistory.objects.all() | ||
self.assertEqual(len(archive_reviews), 2) | ||
|
||
call_command( | ||
'set_is_attempt_active', | ||
batch_size=5, | ||
sleep_time=0 | ||
) | ||
|
||
review = ProctoredExamSoftwareSecureReview.objects.get(attempt_code=self.attempt['attempt_code']) | ||
self.assertFalse(review.is_attempt_active) | ||
|
||
archive_reviews = ProctoredExamSoftwareSecureReviewHistory.objects.filter(is_attempt_active=False) | ||
self.assertEqual(len(archive_reviews), 2) |
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