""" Document Management Service Ce module fournit des services pour gérer les documents dans le système de gestion documentaire. Il gère la logique de versioning pour les tags uniques et l'ajout de nouveaux documents. """ from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.utils.translation import gettext_lazy as _, get_language from .models import ( DocumentFolder, DocumentFolderAttachment, DocumentTag, DocumentVersion, ManagedDocument, ) class DocumentManagementService: """ Service pour gérer l'ajout de documents avec la logique de versioning. Principe: - Pour les tags uniques (is_unique=True): si un document avec ce tag existe déjà dans le répertoire ou ses sous-répertoires, on ajoute une nouvelle version. - Pour les tags non-uniques: on crée un nouveau document. """ @staticmethod def get_folder_and_descendants(folder): """ Récupère un répertoire et tous ses descendants (récursivement). Args: folder: DocumentFolder - Le répertoire racine Returns: set: Ensemble des IDs de répertoires (racine + descendants) """ if folder is None: return set() folder_ids = {folder.pk} stack = [folder.pk] visited = set() # Optimisation: charger toutes les relations enfant-parent en une seule requête # au lieu de faire une requête par niveau all_child_relations = {} current_batch = [folder.pk] while current_batch: # Charger les enfants de tous les folders du batch actuel en une seule requête child_pairs = DocumentFolder.child_folders.through.objects.filter( to_documentfolder_id__in=current_batch ).values_list('to_documentfolder_id', 'from_documentfolder_id') next_batch = [] for parent_id, child_id in child_pairs: if parent_id not in all_child_relations: all_child_relations[parent_id] = [] all_child_relations[parent_id].append(child_id) if child_id not in visited: folder_ids.add(child_id) visited.add(child_id) next_batch.append(child_id) current_batch = next_batch return folder_ids @staticmethod def find_existing_document_with_tag(folder, tag, unique_params=None): """ Recherche un document existant avec un tag spécifique dans un répertoire et tous ses sous-répertoires (récursivement). Args: folder: DocumentFolder - Le répertoire racine à rechercher tag: DocumentTag - Le tag à rechercher unique_params: dict - Paramètres additionnels d'unicité (optionnel) Exemple: {'format': 'pdf'} ou {'cables': 'C1,C2'} Returns: ManagedDocument ou None: Le document existant ou None """ if folder is None or tag is None: return None folder_ids = DocumentManagementService.get_folder_and_descendants(folder) # Normaliser les paramètres normalized_params = unique_params or {} # Rechercher un document avec ce tag dans ces répertoires documents = ( ManagedDocument.objects .filter(folders__pk__in=folder_ids, tags=tag, is_archived=False) .order_by("-updated_at") ) # Filtrer par paramètres d'unicité for document in documents: doc_params = document.unique_params or {} if doc_params == normalized_params: return document return None @staticmethod def get_default_folder_for_object(content_object): """ Récupère le répertoire par défaut associé à un objet. Args: content_object: L'objet (ex: Structure) pour lequel récupérer le répertoire Returns: DocumentFolder ou None """ if content_object is None: return None content_type = ContentType.objects.get_for_model(content_object) folder_attachment = ( DocumentFolderAttachment.objects .filter( content_type=content_type, object_id=content_object.pk, is_default=True ) .select_related("folder") .first() ) if folder_attachment: return folder_attachment.folder # Fallback: chercher n'importe quel répertoire attaché folder_attachment = ( DocumentFolderAttachment.objects .filter( content_type=content_type, object_id=content_object.pk ) .select_related("folder") .first() ) return folder_attachment.folder if folder_attachment else None @staticmethod def get_subfolders_for_object(content_object): """ Récupère tous les sous-répertoires disponibles pour un objet. Args: content_object: L'objet pour lequel récupérer les sous-répertoires Returns: list: Liste de dictionnaires avec les infos des répertoires """ default_folder = DocumentManagementService.get_default_folder_for_object(content_object) if default_folder is None: return [] folder_ids = DocumentManagementService.get_folder_and_descendants(default_folder) folders = ( DocumentFolder.objects .filter(pk__in=folder_ids) .order_by("name") ) result = [] for folder in folders: # Calculer le chemin relatif path_parts = [] current = folder while current and current.pk != default_folder.pk: path_parts.insert(0, current.name) parents = list(current.parent_folders.all()) current = parents[0] if parents else None if folder.pk == default_folder.pk: display_path = folder.name + " (racine)" else: display_path = " / ".join(path_parts) result.append({ "id": folder.pk, "name": folder.name, "path": display_path, "is_root": folder.pk == default_folder.pk, }) return result @staticmethod def get_available_tags_for_thematic(thematic_code): """ Récupère les tags disponibles pour une thématique donnée. Args: thematic_code: str - Le code de la thématique Returns: QuerySet: Tags disponibles """ tags = DocumentTag.objects.filter(thematics__code=thematic_code) lang = get_language() if lang == 'nl': return tags.order_by("name_nl") return tags.order_by("name_fr") @staticmethod def can_user_add_unique_tag_document(user, tag): """ Vérifie si un utilisateur peut ajouter un document avec un tag unique. Args: user: User - L'utilisateur tag: DocumentTag - Le tag Returns: bool: True si autorisé, False sinon """ if not tag.is_unique: return True user_config = getattr(user, "config", None) if user_config is None: return False # Les utilisateurs internes peuvent toujours ajouter des documents avec tags uniques if user_config.is_intern: return True # Les utilisateurs externes doivent avoir la permission explicite return user_config.can_add_unique_tag_documents @staticmethod @transaction.atomic def add_document( file, tag, folder, user, title=None, description=None, content_object=None, target_folder=None, unique_params=None ): """ Ajoute un document avec la logique de versioning appropriée. Args: file: Le fichier à uploader tag: DocumentTag - Le tag du document folder: DocumentFolder - Le répertoire racine (pour la recherche) user: User - L'utilisateur qui ajoute le document title: str - Titre du document (optionnel) description: str - Description (optionnel) content_object: L'objet à attacher (optionnel) target_folder: DocumentFolder - Le répertoire cible spécifique (optionnel) unique_params: dict - Paramètres additionnels d'unicité (optionnel) Exemple: {'cables': 'C1,C2'} Ces paramètres sont fusionnés avec le format de fichier détecté automatiquement depuis l'extension du fichier. Le format est TOUJOURS inclus (ex: 'pdf', 'dxf', 'dwg'). L'unicité est donc liée au tag + format (+ paramètres extra). Returns: dict: { "document": ManagedDocument, "version": DocumentVersion, "is_new_version": bool, "message": str } """ import os from .models import DocumentAttachment # Déterminer le répertoire cible if target_folder is None: target_folder = folder # Normaliser les paramètres normalized_params = dict(unique_params) if unique_params else {} # Injecter automatiquement le format depuis l'extension du fichier # L'unicité est toujours liée au tag ET au format, pour tous les tags. if unique_params != {}: if hasattr(file, 'name') and 'format' not in normalized_params: _base, ext = os.path.splitext(file.name) if ext: normalized_params['format'] = ext[1:].lower() # Pour les tags uniques, rechercher un document existant existing_document = None if tag and tag.is_unique: existing_document = DocumentManagementService.find_existing_document_with_tag( folder, tag, normalized_params ) if existing_document: # Ajouter une nouvelle version au document existant version = DocumentVersion( document=existing_document, file=file, uploaded_by=user, description=description or "", comment=_("New version added"), status=DocumentVersion.STATUS_PENDING, ) version.save() return { "document": existing_document, "version": version, "is_new_version": True, "message": _("New version added to existing document."), } else: # Créer un nouveau document doc_title = title or (file.name if hasattr(file, "name") else _("Document")) document = ManagedDocument.objects.create( title=doc_title, description=description or "", created_by=user, unique_params=normalized_params, ) # Ajouter le tag if tag: document.tags.add(tag) # Ajouter au répertoire if target_folder: document.folders.add(target_folder) # Créer la première version version = DocumentVersion( document=document, file=file, uploaded_by=user, description=description or "", comment=_("Initial version"), status=DocumentVersion.STATUS_PENDING, ) version.save() # Attacher à l'objet de contexte si fourni if content_object: content_type = ContentType.objects.get_for_model(content_object) DocumentAttachment.objects.get_or_create( document=document, content_type=content_type, object_id=content_object.pk, defaults={"attached_by": user}, ) return { "document": document, "version": version, "is_new_version": False, "message": _("New document created."), } @staticmethod def get_documents_for_asset(content_object, tags_to_show=None): """ Récupère les documents associés à un asset avec filtrage par tags. Args: content_object: L'objet (ex: Structure) tags_to_show: QuerySet ou liste de tags à afficher (optionnel) Returns: dict: { "main_documents": Liste des documents avec tags show_in_asset_detail, "other_documents": Liste des autres documents, "folder": Le répertoire par défaut } """ from .models import DocumentAttachment folder = DocumentManagementService.get_default_folder_for_object(content_object) if folder is None: return { "main_documents": [], "other_documents": [], "folder": None, } folder_ids = DocumentManagementService.get_folder_and_descendants(folder) # Récupérer tous les documents dans ces répertoires documents = ( ManagedDocument.objects .filter(folders__pk__in=folder_ids, is_archived=False) .select_related("current_version") .prefetch_related("tags", "folders") .distinct() .order_by("-updated_at") ) # Séparer les documents principaux (avec tags show_in_asset_detail) main_documents = [] other_documents = [] # Tags à afficher dans le détail if tags_to_show is None: tags_to_show = DocumentTag.objects.filter(show_in_asset_detail=True) tag_ids_to_show = set(tags_to_show.values_list("pk", flat=True)) # Optimisation: pré-charger les tags avec prefetch_related élimine les requêtes N+1 # Mais on doit itérer sur les documents de manière efficace for doc in documents: # Les tags sont déjà chargés via prefetch_related, donc pas de requête ici doc_tags = list(doc.tags.all()) doc_tag_ids = {tag.pk for tag in doc_tags} if doc_tag_ids & tag_ids_to_show: # Associer chaque tag principal au document for tag in doc_tags: if tag.pk in tag_ids_to_show: main_documents.append({ "document": doc, "tag": tag, "version": doc.latest_version, }) else: # Inclure le premier tag disponible pour l'affichage (badge/historique) primary_tag = doc_tags[0] if doc_tags else None other_documents.append({ "document": doc, "tag": primary_tag, "version": doc.latest_version, }) return { "main_documents": main_documents, "other_documents": other_documents, "folder": folder, } @staticmethod def get_document_versions_by_tag(content_object, tag_slug, unique_params=None): """ Récupère l'historique des versions d'un document avec un tag spécifique. Args: content_object: L'objet (ex: Structure) tag_slug: str - Le slug du tag unique_params: dict - Paramètres additionnels d'unicité (optionnel) Returns: list: Liste des versions du document """ folder = DocumentManagementService.get_default_folder_for_object(content_object) if folder is None: return [] try: tag = DocumentTag.objects.get(slug=tag_slug) except DocumentTag.DoesNotExist: return [] document = DocumentManagementService.find_existing_document_with_tag( folder, tag, unique_params ) if document is None: return [] return list( document.versions .select_related("uploaded_by") .order_by("-version_number") ) @staticmethod def get_document_history_by_tag(content_object, tag_slug, unique_params=None): """ Alias pour get_document_versions_by_tag. Récupère l'historique des versions d'un document avec un tag spécifique. Args: content_object: L'objet (ex: Structure, TrafficLightIntersection) tag_slug: str - Le slug du tag unique_params: dict - Paramètres additionnels d'unicité (optionnel) Returns: list: Liste des versions du document """ return DocumentManagementService.get_document_versions_by_tag( content_object, tag_slug, unique_params ) @staticmethod def get_all_documents_by_tag(content_object, tag_slug): """ Récupère tous les documents avec un tag spécifique et leurs paramètres d'unicité. Args: content_object: L'objet (ex: Structure) tag_slug: str - Le slug du tag Returns: list: Liste de dictionnaires contenant les documents et leurs paramètres [{"document": ManagedDocument, "unique_params": dict}, ...] """ folder = DocumentManagementService.get_default_folder_for_object(content_object) if folder is None: return [] try: tag = DocumentTag.objects.get(slug=tag_slug) except DocumentTag.DoesNotExist: return [] folder_ids = DocumentManagementService.get_folder_and_descendants(folder) documents = ( ManagedDocument.objects .filter(folders__pk__in=folder_ids, tags=tag, is_archived=False) .select_related("current_version") .order_by("-updated_at") ) return [ { "document": doc, "unique_params": doc.unique_params or {}, "version": doc.latest_version, } for doc in documents ] .order_by("-version_number")