Skip to content

Commit

Permalink
Merge pull request #956 from edx/alangsto/review_status_update_command
Browse files Browse the repository at this point in the history
feat: management command for triggering a status update to exam attempt
  • Loading branch information
alangsto committed Sep 3, 2021
2 parents 2f5945f + c3c48b5 commit b2819b8
Show file tree
Hide file tree
Showing 5 changed files with 193 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
~~~~~~~~~~

[3.24.5] - 2021-09-02
~~~~~~~~~~~~~~~~~~~~~
* Add management command for updating an attempt status based on its associated review

[3.24.4] - 2021-09-02
~~~~~~~~~~~~~~~~~~~~~
* Add testing for exam attempt email failure and related logging
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.24.4'
__version__ = '3.24.5'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
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)
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)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@edx/edx-proctoring",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
"version": "3.24.4",
"version": "3.24.5",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"
Expand Down

0 comments on commit b2819b8

Please sign in to comment.