feat(assets): add create_structure_folders management command

This commit is contained in:
kdeterme 2026-09-21 15:14:26 +02:00
parent 9726e8b618
commit ef40e48163

View file

@ -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 : "<code_ouvrage> - <nom_ouvrage>" (ex: "MS001 - Station Albert")
- Slug : "<code_minuscule>" (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}"
))