diff --git a/management/commands/merge_club.py b/management/commands/merge_club.py new file mode 100644 index 0000000000000000000000000000000000000000..50f698b2d06eadb917c0a20f3ecc34e6b554cbe0 --- /dev/null +++ b/management/commands/merge_club.py @@ -0,0 +1,298 @@ +# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import getpass +from time import sleep + +from django.conf import settings +from django.core.mail import mail_admins +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q +from django.test import override_settings +from note.models import Alias, Transaction, TransactionTemplate +from member.models import Club, Membership + + +class Command(BaseCommand): + """ + This script is used to merge clubs. + THIS IS DANGEROUS SCRIPT, use it only if you know what you do !!! + """ + + def add_arguments(self, parser): + parser.add_argument('--fake_club', '-c', type=str, nargs='+', help="Club id to merge and delete.") + parser.add_argument('--true_club', '-C', type=str, help="Club id will not be deleted.") + parser.add_argument('--force', '-f', action='store_true', + help="Force the script to have low verbosity.") + parser.add_argument('--doit', '-d', action='store_true', + help="Don't ask for a final confirmation and commit modification. " + "This option should really be used carefully.") + + def handle(self, *args, **kwargs): + force = kwargs['force'] + + if not force: + self.stdout.write(self.style.WARNING("This is a dangerous script. " + "Please use --force to indicate that you known what you are doing. " + "Nothing will be deleted yet.")) + sleep(5) + + # We need to know who to blame. + qs = User.objects.filter(note__alias__normalized_name=Alias.normalize(getpass.getuser())) + if not qs.exists(): + self.stderr.write(self.style.ERROR("I don't know who you are. Please add your linux id as an alias of " + "your own account.")) + exit(2) + executor = qs.get() + + deleted_clubs = [] + deleted = [] + created = [] + edited = [] + + # Don't send mails during the process + with override_settings(EMAIL_BACKEND='django.core.mail.backends.dummy.EmailBackend'): + true_club_id = kwargs['true_club'] + if true_club_id.isnumeric(): + qs = Club.objects.filter(pk=int(true_club_id)) + if not qs.exists(): + self.stderr.write(self.style.WARNING(f"Club {true_club_id} was not found. Aborted…")) + exit(2) + true_club = qs.get() + else: + qs = Alias.objects.filter(normalized_name=Alias.normalize(true_club_id), note__noteclub__isnull=False) + if not qs.exists(): + self.stderr.write(self.style.WARNING(f"Club {true_club_id} was not found. Aborted…")) + exit(2) + true_club = qs.get().note.club + + fake_clubs = [] + for fake_club_id in kwargs['fake_club']: + if fake_club_id.isnumeric(): + qs = Club.objects.filter(pk=int(fake_club_id)) + if not qs.exists(): + self.stderr.write(self.style.WARNING(f"Club {fake_club_id} was not found. Ignoring…")) + continue + fake_clubs.append(qs.get()) + else: + qs = Alias.objects.filter(normalized_name=Alias.normalize(fake_club_id), note__noteclub__isnull=False) + if not qs.exists(): + self.stderr.write(self.style.WARNING(f"Club {fake_club_id} was not found. Ignoring…")) + continue + fake_clubs.append(qs.get().note.club) + + clubs = fake_clubs.copy() + clubs.append(true_club) + for club in fake_clubs: + children = Club.objects.filter(parent_club=club) + for child in children: + if child not in fake_clubs: + self.stderr.write(self.style.ERROR(f"Club {club} has child club {child} which are not selected for merge. Aborted.")) + exit(1) + + with transaction.atomic(): + local_deleted = [] + local_created = [] + local_edited = [] + + # Unlock note to enable modifications + for club in clubs: + if force and not club.note.is_active: + club.note.is_active = True + club.note.save() + + # Deleting objects linked to fake_club and true_club + + # Deleting transactions + # We delete transaction : + # fake_club_i <-> fake_club_j + # fake_club_i <-> true_club + transactions = Transaction.objects.filter(Q(source__noteclub__club__in=clubs) + & Q(destination__noteclub__club__in=clubs)).all() + local_deleted += list(transactions) + for tr in transactions: + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Removing {tr}…") + if force: + tr.delete() + + # Merge buttons + buttons = TransactionTemplate.objects.filter(destination__club__in=fake_clubs) + local_edited += list(buttons) + for b in buttons: + b.destination = true_club.note + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Edit {b}") + if force: + b.save() + + # Merge transactions + transactions = Transaction.objects.filter(source__noteclub__club__in=fake_clubs) + local_deleted += list(transactions) + for tr in transactions: + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Removing {tr}…") + tr_merge = tr + tr_merge.source = true_club.note + local_created.append(tr_merge) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Creating {tr_merge}…") + if force: + if not tr.destination.is_active: + tr.destination.is_active = True + tr.destination.save() + tr.delete() + tr_merge.save() + tr.destination.is_active = False + tr.destination.save() + else: + tr.delete() + tr_merge.save() + transactions = Transaction.objects.filter(destination__noteclub__club__in=fake_clubs) + local_deleted += list(transactions) + for tr in transactions: + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Removing {tr}…") + tr_merge = tr + tr_merge.destination = true_club.note + local_created.append(tr_merge) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Creating {tr_merge}…") + if force: + if not tr.source.is_active: + tr.source.is_active = True + tr.source.save() + tr.delete() + tr_merge.save() + tr.source.is_active = False + tr.source.save() + else: + tr.delete() + tr_merge.save() + if 'permission' in settings.INSTALLED_APPS: + from permission.models import Role + r = Role.objects.filter(for_club__in=fake_clubs) + for role in r: + role.for_club = true_club + local_edited.append(role) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Edit {role}…") + if force: + role.save() + + # Merge memberships + for club in fake_clubs: + memberships = Membership.objects.filter(club=club) + local_edited += list(memberships) + for membership in memberships: + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Edit {membership}…") + if force: + membership.club = true_club + membership.save() + + # Merging aliases + alias_list = [] + for fake_club in fake_clubs: + alias_list += list(fake_club.note.alias.all()) + local_deleted += alias_list + for alias in alias_list: + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Removing alias {alias}…") + alias_merge = alias + alias_merge.note = true_club.note + local_created.append(alias_merge) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Creating alias {alias_merge}…") + if force: + alias.delete() + alias_merge.save() + + if 'activity' in settings.INSTALLED_APPS: + from activity.models import Activity + + # Merging activities + activities = Activity.objects.filter(organizer__in=fake_clubs) + for act in activities: + act.organizer = true_club + local_edited.append(act) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Edit {act}…") + if force: + act.save() + activities = Activity.objects.filter(attendees_club__in=fake_clubs) + for act in activities: + act.attendees_club = true_club + local_edited.append(act) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Edit {act}…") + if force: + act.save() + + if 'food' in settings.INSTALLED_APPS: + from food.models import Food + foods = Food.objects.filter(owner__in=fake_clubs) + for f in foods: + f.owner = true_club + local_edited.append(f) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Edit {f}…") + if force: + f.save() + + if 'wrapped' in settings.INSTALLED_APPS: + from wrapped.models import Wrapped + wraps = Wrapped.objects.filter(note__noteclub__club__in=fake_clubs) + local_deleted += list(wraps) + for w in wraps: + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Remove {w}…") + if force: + w.delete() + + # Deleting note + for club in fake_clubs: + local_deleted.append(club.note) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Remove note of {club}…") + if force: + club.note.delete() + + # Finally deleting user + for club in fake_clubs: + local_deleted.append(club) + if kwargs['verbosity'] >= 1: + self.stdout.write(f"Remove {club}…") + if force: + club.delete() + + # This script should really not be used. + if not kwargs['doit'] and not input('You are about to delete real user data. ' + 'Are you really sure that it is what you want? [y/N] ')\ + .lower().startswith('y'): + self.stdout.write(self.style.ERROR("Aborted.")) + exit(1) + + if kwargs['verbosity'] >= 1: + for club in fake_clubs: + self.stdout.write(self.style.SUCCESS(f"Club {club} deleted and merge in {true_club}.")) + deleted_clubs.append(clubs) + deleted += local_deleted + created += local_created + edited += local_edited + + if deleted_clubs: + message = f"Les clubs {deleted_clubs} ont été supprimé⋅es pour être fusionné dans le club {true_club} par {executor}.\n\n" + message += "Ont été supprimés en conséquence les objets suivants :\n\n" + for obj in deleted: + message += f"{repr(obj)} (pk: {obj.pk})\n" + message += "\n\nOnt été créés en conséquence les objects suivants :\n\n" + for obj in created: + message += f"{repr(obj)} (pk: {obj.pk})\n" + message += "\n\nOnt été édités en conséquence les objects suivants :\n\n" + for obj in edited: + message += f"{repr(obj)} (pk: {obj.pk})\n" + if force and kwargs['doit']: + mail_admins("Clubs fusionnés", message)