diff --git a/.gitignore b/.gitignore index 724712a47..21b9a95d3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ node_modules/ #IDE .vscode/ +.cursor/ #temporaire /.tgitconfig diff --git a/conventions/management/commands/cleanup_ddt.py b/conventions/management/commands/cleanup_ddt.py new file mode 100644 index 000000000..d0f842faf --- /dev/null +++ b/conventions/management/commands/cleanup_ddt.py @@ -0,0 +1,129 @@ +from django.core.management.base import BaseCommand + +from instructeurs.models import Administration + + +class Command(BaseCommand): + help = "Foo" + + def add_arguments(self, parser): + parser.add_argument( + "--verbose", + help="Verbose output", + action="store_true", + default=False, + ) + parser.add_argument( + "--dry-run", + help="Dry run", + action="store_true", + default=False, + ) + + def handle(self, *args, **options): + verbose = options["verbose"] + dry_run = options["dry_run"] + + ddt = Administration.objects.filter(code__startswith="DD").exclude( + code__startswith="DDI" + ) + self.stdout.write(self.style.SUCCESS(f"## {ddt.count()} DDT")) + self.stdout.write(self.style.SUCCESS("")) + if verbose: + for d in ddt.order_by("code"): + self.stdout.write(self.style.SUCCESS(f"{d.code} - {d.nom}")) + self.stdout.write(self.style.SUCCESS("")) + ddi = Administration.objects.filter(code__startswith="DDI") + self.stdout.write(self.style.SUCCESS(f"\n## {ddi.count()} DDI\n")) + self.stdout.write(self.style.SUCCESS("")) + if verbose: + for d in ddi.order_by("code"): + self.stdout.write(self.style.SUCCESS(f"{d.code} - {d.nom}")) + ddi_codes = [d.code for d in ddi] + ddi_by_code = {d.code: d for d in ddi} + ddt_ddi = {} + for dd in ddt: + ddi_code = dd.code.replace("DD", "DDI") + if ddi_code in ddi_codes: + ddt_ddi[dd.code] = ddi_by_code[ddi_code] + else: + self.stdout.write( + self.style.WARNING( + f"{dd.code} - {dd.nom} doesn't have a DDI equivalence" + ) + ) + nb_conventions_total = 0 + self.stdout.write(self.style.SUCCESS("")) + self.stdout.write(self.style.SUCCESS("## Détails par DDT")) + self.stdout.write(self.style.SUCCESS("")) + for d in ddt.order_by("code"): + nb_conventions_ddt = 0 + conventions = [] + self.stdout.write(self.style.SUCCESS("")) + self.stdout.write(self.style.SUCCESS(f"### {d.code} - {d.nom}")) + self.stdout.write(self.style.SUCCESS("")) + if nb_programme := d.programmes.all().count(): + self.stdout.write( + self.style.SUCCESS( + f"{d.code} - {d.nom} : {nb_programme} programmes" + ) + ) + nb_users = len(list(set([r.user for r in d.roles.all()]))) + if nb_users > 1: + self.stdout.write( + self.style.NOTICE(f"{d.code} - {d.nom} : {nb_users} users") + ) + else: + self.stdout.write( + self.style.SUCCESS(f"{d.code} - {d.nom} : {nb_users} users") + ) + for p in d.programmes.all(): + for c in p.conventions.all(): + nb_conventions_ddt += 1 + conventions.append(c) + self.stdout.write( + self.style.SUCCESS( + f"{d.code} - {d.nom} : {nb_conventions_ddt} conventions" + ) + ) + if verbose: + self.stdout.write(self.style.SUCCESS("")) + self.stdout.write(self.style.SUCCESS("#### Conventions")) + self.stdout.write(self.style.SUCCESS("")) + for c in conventions: + self.stdout.write(self.style.SUCCESS(f"{c} ({c.uuid})")) + nb_conventions_total += nb_conventions_ddt + self.stdout.write(self.style.SUCCESS(f"{nb_conventions_total} conventions")) + + self.stdout.write(self.style.WARNING("")) + self.stdout.write(self.style.WARNING("Début de ré-attribution")) + for d in ddt.order_by("code"): + self.stdout.write(self.style.WARNING("")) + self.stdout.write( + self.style.WARNING( + f"Début de ré-attribution pour la DDT {d.code} - {d.nom} vers" + f" {ddt_ddi[d.code].code} ({ddt_ddi[d.code].nom})" + ) + ) + self.stdout.write(self.style.WARNING("")) + for p in d.programmes.all(): + if verbose: + self.stdout.write( + self.style.SUCCESS( + f"Ré-attribution de {p} de {d.code} vers {ddt_ddi[d.code].code}" + ) + ) + if not dry_run: + p.administration_id = ddt_ddi[d.code].id + p.save() + if d.programmes.all().count() != 0 and not dry_run: + raise Exception( + f"La DDT {d.code} - {d.nom} a plusieurs programmes après" + f" ré-attribution : {d.programmes.all()}" + ) + self.stdout.write(self.style.SUCCESS("")) + self.stdout.write( + self.style.SUCCESS(f"Suppression de la DDT {d.code} - {d.nom}") + ) + if not dry_run: + d.delete() diff --git a/programmes/migrations/0128_alter_programme_administration.py b/programmes/migrations/0128_alter_programme_administration.py new file mode 100644 index 000000000..ac20bf69f --- /dev/null +++ b/programmes/migrations/0128_alter_programme_administration.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.4 on 2025-08-12 06:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("instructeurs", "0001_initial_squashed_0017_auto_20230925_1209"), + ("programmes", "0127_alter_typestationnement_typologie"), + ] + + operations = [ + migrations.AlterField( + model_name="programme", + name="administration", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="programmes", + to="instructeurs.administration", + ), + ), + ] diff --git a/programmes/models/models.py b/programmes/models/models.py index a89edc116..87a1d9944 100644 --- a/programmes/models/models.py +++ b/programmes/models/models.py @@ -69,7 +69,11 @@ class Meta: null=False, ) administration = models.ForeignKey( - "instructeurs.Administration", on_delete=models.SET_NULL, null=True, blank=True + "instructeurs.Administration", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="programmes", ) adresse = models.TextField(blank=True) code_postal = models.CharField(max_length=5, blank=True)