Skip to content

Commit

Permalink
Merge pull request #752 from edx/alangsto/management_command
Browse files Browse the repository at this point in the history
Added management command for updating is_attempt_active field
  • Loading branch information
alangsto authored Jan 15, 2021
2 parents 5543663 + 6bedc76 commit fc4941a
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Change Log
Unreleased
~~~~~~~~~~

[2.5.10] - 2021-01-15
~~~~~~~~~~~~~~~~~~~~~
* Added management command to update `is_attempt_active` field on review models

[2.5.9] - 2021-01-13
~~~~~~~~~~~~~~~~~~~~
* Added `is_attempt_active` field to ProctoredExamSoftwareSecureReview and
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__ = '2.5.9'
__version__ = '2.5.10'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
104 changes: 104 additions & 0 deletions edx_proctoring/management/commands/set_is_attempt_active.py
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
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)
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": "2.5.9",
"version": "2.5.10",
"main": "edx_proctoring/static/index.js",
"repository": {
"type": "git",
Expand Down

0 comments on commit fc4941a

Please sign in to comment.