loko/streetup/documents/services.py
2026-07-22 14:48:40 +02:00

553 lines
19 KiB
Python

"""
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")