From ef40e481639448676dd87b2af6e9ed846bbb1664 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Mon, 21 Sep 2026 15:14:26 +0200 Subject: [PATCH] feat(assets): add create_structure_folders management command --- .../commands/create_structure_folders.py | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 loko/assets/management/commands/create_structure_folders.py diff --git a/loko/assets/management/commands/create_structure_folders.py b/loko/assets/management/commands/create_structure_folders.py new file mode 100644 index 0000000..2af7ef9 --- /dev/null +++ b/loko/assets/management/commands/create_structure_folders.py @@ -0,0 +1,210 @@ +""" +Management command: create_structure_folders +=========================================== +Crée les répertoires par défaut de gestion documentaire pour les structures (ouvrages d'art) +qui n'en possèdent pas encore (ex: stations et tunnels de métro récemment importés). + +Pour chaque ouvrage d'art traité : +1. Crée le dossier racine rattaché à l'ouvrage via DocumentFolderAttachment (is_default=True) : + - Nom : " - " (ex: "MS001 - Station Albert") + - Slug : "" (ex: "ms001") + - Dossier parent : "Structures" (slug: "assets-structures") + - Thématique : "structures" + - Visibilité : "internal" +2. Crée l'arborescence standard de sous-dossiers : + - Civil engineering + ├── Inspection reports + └── Exceptional transport + - Electromechanics + └── Preventive maintenance + +Usage : + python manage.py create_structure_folders [--dry-run] [--type TYPE] [--code CODE] +""" + +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand +from django.db import transaction +from django.utils.text import slugify + +from assets.models.structures import Structure +from common.models import Thematic +from documents.models import DocumentFolder, DocumentFolderAttachment, VisibilityScope + +User = get_user_model() + + +def ensure_structure_folders(structure, creator_user=None, dry_run=False): + """ + Crée ou s'assure de l'existence du dossier racine et des sous-dossiers + standards pour un ouvrage d'art (Structure). + Idempotent : ne recrée pas ce qui existe déjà. + """ + structure_ct = ContentType.objects.get_for_model(Structure) + structures_thematic = Thematic.objects.filter(code='structures').first() + structures_parent_folder = DocumentFolder.objects.filter(slug='assets-structures').first() + + root_slug = slugify(structure.code) + root_folder = DocumentFolder.objects.filter(slug=root_slug).first() + + root_attachment = DocumentFolderAttachment.objects.filter( + content_type=structure_ct, + object_id=structure.pk, + is_default=True + ).select_related('folder').first() + + if not root_folder and root_attachment: + root_folder = root_attachment.folder + + root_name = f"{structure.code} - {structure.get_name()}" + root_desc = f"Dossier pour la structure {structure.code} - {structure.get_name()}" + + created_root = False + if not root_folder: + if not dry_run: + root_folder = DocumentFolder.objects.create( + name=root_name, + slug=root_slug, + description=root_desc, + visibility=VisibilityScope.INTERNAL, + is_predefined=True, + application_label='structures', + created_by=creator_user + ) + if structures_thematic: + root_folder.thematics.add(structures_thematic) + if structures_parent_folder: + root_folder.parent_folders.add(structures_parent_folder) + created_root = True + + if not dry_run and root_folder and not root_attachment: + DocumentFolderAttachment.objects.get_or_create( + content_type=structure_ct, + object_id=structure.pk, + folder=root_folder, + defaults={'is_default': True} + ) + + subfolders_created = 0 + + def get_or_create_subfolder(parent, name, sub_suffix): + nonlocal subfolders_created + base_slug = f"{root_slug}-{sub_suffix}"[:150] + sub = DocumentFolder.objects.filter(slug=base_slug).first() + if not sub and parent and getattr(parent, 'pk', None): + sub = parent.child_folders.filter(name=name).first() + + if not sub: + if not dry_run: + sub = DocumentFolder.objects.create( + name=name, + slug=base_slug, + visibility=VisibilityScope.INTERNAL, + is_predefined=False, + application_label='structures', + created_by=creator_user + ) + if structures_thematic: + sub.thematics.add(structures_thematic) + if parent and getattr(parent, 'pk', None): + sub.parent_folders.add(parent) + subfolders_created += 1 + return sub, True + return sub, False + + # 1. Civil engineering & sous-dossiers + civil_folder, _ = get_or_create_subfolder(root_folder, "Civil engineering", "civil-engineering") + get_or_create_subfolder(civil_folder, "Inspection reports", "civil-engineering-inspection-reports") + get_or_create_subfolder(civil_folder, "Exceptional transport", "civil-engineering-exceptional-transport") + + # 2. Electromechanics & sous-dossiers + em_folder, _ = get_or_create_subfolder(root_folder, "Electromechanics", "electromechanics") + get_or_create_subfolder(em_folder, "Preventive maintenance", "electromechanics-preventive-maintenance") + + return { + 'root_folder': root_folder, + 'created_root': created_root, + 'subfolders_created': subfolders_created + } + + +class Command(BaseCommand): + help = "Crée les répertoires par défaut de gestion documentaire pour les structures." + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + help="Simule l'exécution sans effectuer d'écritures en base de données." + ) + parser.add_argument( + '--type', + dest='structure_type', + help="Filtre par type de structure (ex: metro_station,metro_tunnel)." + ) + parser.add_argument( + '--code', + dest='structure_code', + help="Cible un ouvrage spécifique via son code (ex: MS001)." + ) + parser.add_argument( + '--user', + dest='username', + help="Nom d'utilisateur créateur des dossiers (par défaut kdeterme ou superuser)." + ) + + def handle(self, *args, **options): + dry_run = options['dry_run'] + structure_type = options.get('structure_type') + structure_code = options.get('structure_code') + username = options.get('username') + + if dry_run: + self.stdout.write(self.style.WARNING("=== Mode simulation (--dry-run) activé. Aucune modification en base. ===")) + + creator_user = None + if username: + creator_user = User.objects.filter(username=username).first() + if not creator_user: + creator_user = User.objects.filter(username="kdeterme").first() or User.objects.filter(is_superuser=True).first() + + qs = Structure.objects.all().order_by('code') + + if structure_code: + qs = qs.filter(code__iexact=structure_code) + elif structure_type: + types = [t.strip() for t in structure_type.split(',')] + qs = qs.filter(structure_type__in=types) + + structure_ct = ContentType.objects.get_for_model(Structure) + existing_attached_ids = set( + DocumentFolderAttachment.objects.filter( + content_type=structure_ct, + is_default=True + ).values_list('object_id', flat=True) + ) + + missing_structures = [s for s in qs if s.pk not in existing_attached_ids] + + self.stdout.write(f"Structures analysées : {qs.count()} | Sans répertoire par défaut : {len(missing_structures)}") + + total_roots = 0 + total_subfolders = 0 + + target_structures = missing_structures if not structure_code else list(qs) + + for structure in target_structures: + res = ensure_structure_folders(structure, creator_user=creator_user, dry_run=dry_run) + if res['created_root']: + total_roots += 1 + total_subfolders += res['subfolders_created'] + self.stdout.write(f" ✓ [{structure.code}] {structure.get_name()} -> Dossier racine et arborescence configurés") + + status_label = "[Simulation] Répertoires qui seraient créés" if dry_run else "Répertoires créés avec succès" + self.stdout.write(self.style.SUCCESS( + f"\n=== {status_label} ===\n" + f" - Dossiers racines créés : {total_roots}\n" + f" - Sous-dossiers créés : {total_subfolders}\n" + f" - Total dossiers : {total_roots + total_subfolders}" + ))