# 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.contrib.auth.models import User from django.core.mail import mail_admins 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 class Command(BaseCommand): """ This script is used to force delete a user. THIS IS ONLY ATTENDED TO BE USED TO DELETE FAKE ACCOUNTS THAT WERE VALIDATED BY ERRORS. Please use data anonymization if you want to block the account of a user. """ def add_arguments(self, parser): parser.add_argument('user', type=str, nargs='+', help="User id to delete.") 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_users = [] deleted = [] # Don't send mails during the process with override_settings(EMAIL_BACKEND='django.core.mail.backends.dummy.EmailBackend'): for user_id in kwargs['user']: if user_id.isnumeric(): qs = User.objects.filter(pk=int(user_id)) if not qs.exists(): self.stderr.write(self.style.WARNING(f"User {user_id} was not found. Ignoring…")) continue user = qs.get() else: qs = Alias.objects.filter(normalized_name=Alias.normalize(user_id), note__noteuser__isnull=False) if not qs.exists(): self.stderr.write(self.style.WARNING(f"User {user_id} was not found. Ignoring…")) continue user = qs.get().note.user with transaction.atomic(): local_deleted = [] # Unlock note to enable modifications if force and not user.note.is_active: user.note.is_active = True user.note.save() # Deleting transactions transactions = Transaction.objects.filter(Q(source=user.note) | Q(destination=user.note)).all() local_deleted += list(transactions) for tr in transactions: if kwargs['verbosity'] >= 1: self.stdout.write(f"Removing {tr}…") if force: tr.delete() # Deleting memberships memberships = user.memberships.all() local_deleted += list(memberships) if kwargs['verbosity'] >= 1: for membership in memberships: self.stdout.write(f"Removing {membership}…") if force: memberships.delete() # Deleting aliases alias_set = user.note.alias.all() local_deleted += list(alias_set) if kwargs['verbosity'] >= 1: for alias in alias_set: self.stdout.write(f"Removing alias {alias}…") if force: alias_set.delete() if 'activity' in settings.INSTALLED_APPS: from activity.models import Guest, Entry # Deleting activity entries entries = Entry.objects.filter(Q(note=user.note) | Q(guest__inviter=user.note)).all() local_deleted += list(entries) if kwargs['verbosity'] >= 1: for entry in entries: self.stdout.write(f"Removing {entry}…") if force: entries.delete() # Deleting invited guests guests = Guest.objects.filter(inviter=user.note).all() local_deleted += list(guests) if kwargs['verbosity'] >= 1: for guest in guests: self.stdout.write(f"Removing guest {guest}…") if force: guests.delete() if 'treasury' in settings.INSTALLED_APPS: from treasury.models import SogeCredit # Deleting soge credit credits = SogeCredit.objects.filter(user=user).all() local_deleted += list(credits) if kwargs['verbosity'] >= 1: for credit in credits: self.stdout.write(f"Removing {credit}…") if force: credits.delete() # Deleting note local_deleted.append(user.note) if force: user.note.delete() if 'logs' in settings.INSTALLED_APPS: from logs.models import Changelog # Replace log executors by the runner of the script Changelog.objects.filter(user=user).update(user=executor) # Deleting profile local_deleted.append(user.profile) if force: user.profile.delete() # Finally deleting user if force: user.delete() local_deleted.append(user) # 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: self.stdout.write(self.style.SUCCESS(f"User {user} deleted.")) deleted_users.append(user) deleted += local_deleted if deleted_users: message = f"Les utilisateur⋅rices {deleted_users} ont été supprimé⋅es 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" if force and kwargs['doit']: mail_admins("Utilisateur⋅rices supprimés", message)