# 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)