Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add management command to delete exam attempts #1243

Merged
merged 2 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
~~~~~~~~~~

[4.18.3] - 2024-11-04
~~~~~~~~~~~~~~~~~~~~~
* add management command to delete attempts

[4.18.2] - 2024-10-03
~~~~~~~~~~~~~~~~~~~~~
* fix various bugs related to exams configured with removed proctoring backend
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,4 +3,4 @@
"""

# Be sure to update the version number in edx_proctoring/package.json
__version__ = '4.18.2'
__version__ = '4.18.3'
74 changes: 74 additions & 0 deletions edx_proctoring/management/commands/reset_attempts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
Django management command to delete attempts. This command should only be used
to remove attempts that have not been started or completed, as it will not
reset problem state or grade overrides.
"""
import logging
import time

from django.core.management.base import BaseCommand

from edx_proctoring.models import ProctoredExamStudentAttempt

log = logging.getLogger(__name__)


class Command(BaseCommand):
"""
Django Management command to delete attempts.
"""

def add_arguments(self, parser):
parser.add_argument(
'-p',
'--file_path',
metavar='file_path',
dest='file_path',
required=True,
help='Path to file.'
)
parser.add_argument(
'--batch_size',
action='store',
dest='batch_size',
type=int,
default=300,
help='Maximum number of attempt_ids 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']
file_path = options['file_path']

with open(file_path, 'r') as file:
ids_to_delete = file.readlines()

total_deleted = 0

for i in range(0, len(ids_to_delete), batch_size):
batch_to_delete = ids_to_delete[i:i + batch_size]

delete_queryset = ProctoredExamStudentAttempt.objects.filter(
id__in=batch_to_delete
)
deleted_count, _ = delete_queryset.delete()

total_deleted += deleted_count

log.info(f'{deleted_count} attempts deleted.')
time.sleep(sleep_time)

log.info(f'Job completed. {total_deleted} attempts deleted.')
72 changes: 72 additions & 0 deletions edx_proctoring/management/commands/tests/test_reset_attempts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Tests for the reset_attempts management command
"""
from tempfile import NamedTemporaryFile

import ddt

from django.core.management import call_command

from edx_proctoring.api import create_exam
from edx_proctoring.models import ProctoredExamStudentAttempt
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
from edx_proctoring.tests.utils import LoggedInTestCase


@ddt.ddt
class ResetAttemptsTests(LoggedInTestCase):
"""
Coverage of the reset_attempts.py file
"""

def setUp(self):
"""
Build up test data
"""
super().setUp()
self.exam_id = create_exam(
course_id='a/b/c',
content_id='bar',
exam_name='Test Exam',
time_limit_mins=90
)

self.num_attempts = 10

user_list = self.create_batch_users(self.num_attempts)
for user in user_list:
ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.exam_id,
user_id=user.id,
external_id='foo',
status=ProctoredExamStudentAttemptStatus.created,
allowed_time_limit_mins=10,
taking_as_proctored=True,
is_sample_attempt=False
)

@ddt.data(
5,
7,
10,
)
def test_run_command(self, num_to_delete):
"""
Run the management command
"""
ids = list(ProctoredExamStudentAttempt.objects.all().values_list('id', flat=True))[:num_to_delete]

with NamedTemporaryFile() as file:
with open(file.name, 'w') as writing_file:
for num in ids:
writing_file.write(str(num) + '\n')

call_command(
'reset_attempts',
batch_size=2,
sleep_time=0,
file_path=file.name,
)

attempts = ProctoredExamStudentAttempt.objects.all()
self.assertEqual(len(attempts), self.num_attempts - num_to_delete)
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": "4.18.2",
"version": "4.18.3",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"
Expand Down
Loading