From 0fb90bf3281c023a5d879d3194bda4c99473d172 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Tue, 1 Oct 2024 12:52:20 -0400 Subject: [PATCH] Add management command to resume broken project ownership transfers --- .../project_ownership/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../resume_failed_transfers_2_024_25_fix.py | 97 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 kobo/apps/project_ownership/management/__init__.py create mode 100644 kobo/apps/project_ownership/management/commands/__init__.py create mode 100644 kobo/apps/project_ownership/management/commands/resume_failed_transfers_2_024_25_fix.py diff --git a/kobo/apps/project_ownership/management/__init__.py b/kobo/apps/project_ownership/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kobo/apps/project_ownership/management/commands/__init__.py b/kobo/apps/project_ownership/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kobo/apps/project_ownership/management/commands/resume_failed_transfers_2_024_25_fix.py b/kobo/apps/project_ownership/management/commands/resume_failed_transfers_2_024_25_fix.py new file mode 100644 index 0000000000..34e43a49f9 --- /dev/null +++ b/kobo/apps/project_ownership/management/commands/resume_failed_transfers_2_024_25_fix.py @@ -0,0 +1,97 @@ +from django.core.management import call_command +from django.core.management.base import BaseCommand + +from ...models import ( + Transfer, + TransferStatus, + TransferStatusChoices, + TransferStatusTypeChoices, +) +from ...utils import ( + move_media_files, + move_attachments, + rewrite_mongo_userform_id, +) + + +class Command(BaseCommand): + help = ( + 'Resume project ownership transfers done under `2.024.25` which failed ' + 'with error: "Project A : previous_owner -> new_owner is not in progress"' + ) + + def handle(self, *args, **options): + + usernames = set() + verbosity = options['verbosity'] + + for transfer_status in TransferStatus.objects.filter( + status=TransferStatusChoices.FAILED, + status_type=TransferStatusTypeChoices.GLOBAL, + error__icontains='is not in progress', + ).iterator(): + transfer = transfer_status.transfer + if transfer.asset.pending_delete: + if verbosity: + self.stdout.write( + f'Project `{transfer.asset}` is in trash bin, skip it!' + ) + continue + + if not self._validate_whether_transfer_can_be_fixed(transfer): + if verbosity: + self.stdout.write( + f'Project `{transfer.asset}` transfer cannot be fixed automatically' + ) + continue + + if not transfer.asset.has_deployment: + continue + + if verbosity: + self.stdout.write( + f'Resuming `{transfer.asset}` transfer…' + ) + self._move_data(transfer) + move_attachments(transfer) + move_media_files(transfer) + if verbosity: + self.stdout.write( + f'\tDone!' + ) + usernames.add(transfer.invite.recipient.username) + + # Update attachment storage bytes counters + for username in usernames: + call_command( + 'update_attachment_storage_bytes', + verbosity=verbosity, + force=True, + username=username, + ) + + def _move_data(self, transfer: Transfer): + + # Sanity check + asset = transfer.asset + rewrite_mongo_userform_id(transfer) + number_of_submissions = asset.deployment.xform.num_of_submissions + submission_ids = [ + s['_id'] + for s in asset.deployment.get_submissions(asset.owner, fields=['_id']) + ] + + if number_of_submissions == (mongo_document_count := len(submission_ids)): + self.stdout.write(f'\tSuccess: {number_of_submissions} submissions moved!') + else: + missing_count = number_of_submissions - mongo_document_count + self.stdout.write( + f'\t⚠️ Only {mongo_document_count} submissions moved, ' + f'{missing_count} are missing!' + ) + + def _validate_whether_transfer_can_be_fixed(self, transfer: Transfer) -> bool: + original_new_owner_id = transfer.invite.recipient_id + current_owner_id = transfer.asset.owner_id + + return current_owner_id == original_new_owner_id