553 lines
19 KiB
Python
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")
|
|
|