diff --git a/lms/djangoapps/verify_student/management/commands/cleanup_retired_manual_verifications.py b/lms/djangoapps/verify_student/management/commands/cleanup_retired_manual_verifications.py new file mode 100644 index 000000000000..14c2051cdf2e --- /dev/null +++ b/lms/djangoapps/verify_student/management/commands/cleanup_retired_manual_verifications.py @@ -0,0 +1,65 @@ +""" +One-time cleanup: clear PII and delete ManualVerification rows for retired users. +""" + +import logging + +from django.conf import settings +from django.core.management.base import BaseCommand + +from lms.djangoapps.verify_student.models import ManualVerification + +log = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Clears PII then deletes ManualVerification records belonging to retired users. + + Only runs when REDACT_MANUAL_VERIFICATION_HISTORICAL_PII is True. + + Example usage: + $ ./manage.py lms cleanup_retired_manual_verifications + $ ./manage.py lms cleanup_retired_manual_verifications --dry-run + """ + + help = 'Clear PII and delete ManualVerification rows for retired users.' + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + default=False, + help='Log what would be deleted without making any changes.', + ) + + def handle(self, *args, **options): + if not getattr(settings, 'REDACT_MANUAL_VERIFICATION_HISTORICAL_PII', False): + log.warning('Skipping. REDACT_MANUAL_VERIFICATION_HISTORICAL_PII must first be enabled.') + return + + dry_run = options['dry_run'] + + retired_records = ManualVerification.objects.filter( + user__userretirementrequest__isnull=False, + ) + + count = retired_records.count() + if count == 0: + log.info('No ManualVerification records found for retired users.') + return + + log.info('Found %d ManualVerification record(s) for retired users.', count) + + if dry_run: + log.info('[dry-run] %d record(s) would be redacted and deleted. No changes made.', count) + return + + try: + retired_records.update(name='') + retired_records.delete() + except Exception as exc: + log.exception('Failed to redact/delete ManualVerification records: %s', exc) + raise + + log.info('Redacted and deleted %d ManualVerification record(s) for retired users.', count) diff --git a/lms/djangoapps/verify_student/management/commands/tests/test_cleanup_retired_manual_verifications.py b/lms/djangoapps/verify_student/management/commands/tests/test_cleanup_retired_manual_verifications.py new file mode 100644 index 000000000000..47cb5ac6a605 --- /dev/null +++ b/lms/djangoapps/verify_student/management/commands/tests/test_cleanup_retired_manual_verifications.py @@ -0,0 +1,69 @@ +""" +Tests for django admin command `cleanup_retired_manual_verifications` in the verify_student module +""" + +import logging + +from django.core.management import call_command +from django.test import TestCase, override_settings +from testfixtures import LogCapture + +from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.verify_student.models import ManualVerification +from openedx.core.djangoapps.user_api.tests.factories import UserRetirementRequestFactory + +LOGGER_NAME = 'lms.djangoapps.verify_student.management.commands.cleanup_retired_manual_verifications' + + +class TestCleanupRetiredManualVerificationsCommand(TestCase): + """ Tests for django admin command `cleanup_retired_manual_verifications` in the verify_student module """ + + def test_skips_when_redaction_setting_disabled(self): + """ + Test that the command logs a warning and skips cleanup when redaction setting is disabled. + """ + user = UserFactory.create() + ManualVerification.objects.create( + user=user, + name='Retired User Name', + status='approved', + ) + UserRetirementRequestFactory(user=user) + + with override_settings(REDACT_MANUAL_VERIFICATION_HISTORICAL_PII=False): + with LogCapture(LOGGER_NAME, level=logging.WARNING) as logger: + call_command('cleanup_retired_manual_verifications') + + logger.check( + ( + LOGGER_NAME, + 'WARNING', + 'Skipping. REDACT_MANUAL_VERIFICATION_HISTORICAL_PII must first be enabled.', + ), + ) + assert ManualVerification.objects.filter(user=user, name='Retired User Name').exists() + + def test_redacts_and_deletes_retired_records(self): + """ + Test that the command redacts and deletes retired users' records but leaves active users untouched. + """ + retired_user = UserFactory.create() + active_user = UserFactory.create() + + ManualVerification.objects.create( + user=retired_user, + name='Retired User Name', + status='approved', + ) + ManualVerification.objects.create( + user=active_user, + name='Active User Name', + status='approved', + ) + UserRetirementRequestFactory(user=retired_user) + + with override_settings(REDACT_MANUAL_VERIFICATION_HISTORICAL_PII=True): + call_command('cleanup_retired_manual_verifications') + + assert not ManualVerification.objects.filter(user=retired_user).exists() + assert ManualVerification.objects.filter(user=active_user, name='Active User Name').exists()