feat: implement document management service with recursive versioning and folder navigation logic
This commit is contained in:
parent
913c00e34b
commit
ee90cf3173
9 changed files with 353 additions and 46 deletions
|
|
@ -67,6 +67,13 @@ def filter_documents_for_user(
|
||||||
|
|
||||||
# 2. Documents qualifiés par thématiques & contrats (attachments et direct thematics)
|
# 2. Documents qualifiés par thématiques & contrats (attachments et direct thematics)
|
||||||
attached_doc_ids = _get_document_ids_from_objects(user, edit_only=is_edit_check)
|
attached_doc_ids = _get_document_ids_from_objects(user, edit_only=is_edit_check)
|
||||||
|
attached_folder_ids = _get_folder_ids_from_objects(user, edit_only=is_edit_check)
|
||||||
|
if attached_folder_ids:
|
||||||
|
descendant_folder_ids = _collect_descendant_ids(attached_folder_ids)
|
||||||
|
folder_doc_ids = set(
|
||||||
|
queryset.filter(folders__pk__in=descendant_folder_ids).values_list("pk", flat=True)
|
||||||
|
)
|
||||||
|
attached_doc_ids |= folder_doc_ids
|
||||||
|
|
||||||
accessible_thematics = _get_editable_thematic_ids(user) if is_edit_check else _get_accessible_thematic_ids(user)
|
accessible_thematics = _get_editable_thematic_ids(user) if is_edit_check else _get_accessible_thematic_ids(user)
|
||||||
thematic_doc_ids: set[int] = set()
|
thematic_doc_ids: set[int] = set()
|
||||||
|
|
@ -177,6 +184,15 @@ def user_has_document_permission(
|
||||||
if content_object and _user_can_access_content_object(user, content_object, edit_only=is_edit_check):
|
if content_object and _user_can_access_content_object(user, content_object, edit_only=is_edit_check):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# 3. Vérification dossiers attachés à des objets accessibles
|
||||||
|
doc_folders = list(document.folders.all())
|
||||||
|
if doc_folders:
|
||||||
|
doc_folder_ids = {f.pk for f in doc_folders}
|
||||||
|
all_folder_ids = _collect_ancestor_ids(doc_folder_ids)
|
||||||
|
accessible_folder_ids = _get_folder_ids_from_objects(user, edit_only=is_edit_check)
|
||||||
|
if all_folder_ids & accessible_folder_ids:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -231,6 +247,13 @@ def user_has_folder_permission(
|
||||||
if content_object and _user_can_access_content_object(user, content_object, edit_only=is_edit_check):
|
if content_object and _user_can_access_content_object(user, content_object, edit_only=is_edit_check):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Sous-dossier d'un dossier attaché à un objet accessible
|
||||||
|
all_ancestor_ids = _collect_ancestor_ids({folder.pk}) - {folder.pk}
|
||||||
|
if all_ancestor_ids:
|
||||||
|
accessible_folder_ids = _get_folder_ids_from_objects(user, edit_only=is_edit_check)
|
||||||
|
if all_ancestor_ids & accessible_folder_ids:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -605,14 +628,10 @@ def _user_can_access_content_object(user, content_object, edit_only: bool = Fals
|
||||||
if not thematic_code:
|
if not thematic_code:
|
||||||
# Cas des projets ou contrats ou autres modèles génériques
|
# Cas des projets ou contrats ou autres modèles génériques
|
||||||
if content_type.app_label == 'projects' and model_name == 'project':
|
if content_type.app_label == 'projects' and model_name == 'project':
|
||||||
from projects.models import ProjectUserAccess
|
from projects.permissions import can_view_project, can_add_or_edit_project
|
||||||
if ProjectUserAccess.objects.filter(user=user, project=content_object, can_view=True).exists():
|
if edit_only:
|
||||||
return True
|
return can_add_or_edit_project(user, content_object)
|
||||||
if config.is_intern:
|
return can_view_project(user, content_object)
|
||||||
user_them_ids = _get_accessible_thematic_ids(user)
|
|
||||||
if content_object.thematics.filter(pk__in=user_them_ids).exists():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
if content_type.app_label == 'contracts' and model_name == 'contract':
|
if content_type.app_label == 'contracts' and model_name == 'contract':
|
||||||
if not config.limit_assets_to_contracts:
|
if not config.limit_assets_to_contracts:
|
||||||
|
|
@ -725,25 +744,11 @@ def _get_folder_ids_from_objects(user, *, edit_only: bool) -> set[int]:
|
||||||
project_ct = None
|
project_ct = None
|
||||||
|
|
||||||
if project_ct:
|
if project_ct:
|
||||||
from projects.models import Project, ProjectUserAccess
|
from projects.permissions import filter_viewable_projects_for_user
|
||||||
from common.models import UserThematics
|
|
||||||
|
|
||||||
thematic_project_ids: set[int] = set()
|
accessible_project_ids = set(
|
||||||
accessible_thematics = UserThematics.objects.filter(
|
filter_viewable_projects_for_user(user).values_list('pk', flat=True)
|
||||||
user_config=config, can_view_projects=True
|
|
||||||
)
|
)
|
||||||
if config.is_intern and accessible_thematics.exists():
|
|
||||||
accessible_thematic_objs = list(accessible_thematics.values_list('thematic_id', flat=True))
|
|
||||||
thematic_project_ids = set(
|
|
||||||
Project.objects.filter(
|
|
||||||
thematics__pk__in=accessible_thematic_objs
|
|
||||||
).values_list('pk', flat=True)
|
|
||||||
)
|
|
||||||
member_project_ids = set(
|
|
||||||
ProjectUserAccess.objects.filter(user=user, can_view=True).values_list('project_id', flat=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
accessible_project_ids = thematic_project_ids | member_project_ids
|
|
||||||
if accessible_project_ids:
|
if accessible_project_ids:
|
||||||
project_folder_ids = set(
|
project_folder_ids = set(
|
||||||
DocumentFolderAttachment.objects.filter(
|
DocumentFolderAttachment.objects.filter(
|
||||||
|
|
@ -795,25 +800,11 @@ def _get_document_ids_from_objects(user, *, edit_only: bool) -> set[int]:
|
||||||
project_ct = None
|
project_ct = None
|
||||||
|
|
||||||
if project_ct:
|
if project_ct:
|
||||||
from projects.models import Project, ProjectUserAccess
|
from projects.permissions import filter_viewable_projects_for_user
|
||||||
from common.models import UserThematics
|
|
||||||
|
|
||||||
thematic_project_ids: set[int] = set()
|
accessible_project_ids = set(
|
||||||
accessible_thematics = UserThematics.objects.filter(
|
filter_viewable_projects_for_user(user).values_list('pk', flat=True)
|
||||||
user_config=config, can_view_projects=True
|
|
||||||
)
|
)
|
||||||
if config.is_intern and accessible_thematics.exists():
|
|
||||||
accessible_thematic_objs = list(accessible_thematics.values_list('thematic_id', flat=True))
|
|
||||||
thematic_project_ids = set(
|
|
||||||
Project.objects.filter(
|
|
||||||
thematics__pk__in=accessible_thematic_objs
|
|
||||||
).values_list('pk', flat=True)
|
|
||||||
)
|
|
||||||
member_project_ids = set(
|
|
||||||
ProjectUserAccess.objects.filter(user=user, can_view=True).values_list('project_id', flat=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
accessible_project_ids = thematic_project_ids | member_project_ids
|
|
||||||
if accessible_project_ids:
|
if accessible_project_ids:
|
||||||
project_doc_ids = set(
|
project_doc_ids = set(
|
||||||
DocumentAttachment.objects.filter(
|
DocumentAttachment.objects.filter(
|
||||||
|
|
@ -920,6 +911,10 @@ def filter_folders_for_user(
|
||||||
|
|
||||||
# 2. Scope qualifié : thématiques directes ou objets attachés (DocumentFolderAttachment)
|
# 2. Scope qualifié : thématiques directes ou objets attachés (DocumentFolderAttachment)
|
||||||
attached_folder_ids = _get_folder_ids_from_objects(user, edit_only=is_edit_check)
|
attached_folder_ids = _get_folder_ids_from_objects(user, edit_only=is_edit_check)
|
||||||
|
if attached_folder_ids:
|
||||||
|
descendant_folder_ids = _collect_descendant_ids(attached_folder_ids)
|
||||||
|
attached_folder_ids |= descendant_folder_ids
|
||||||
|
|
||||||
accessible_thematics = _get_editable_thematic_ids(user) if is_edit_check else _get_accessible_thematic_ids(user)
|
accessible_thematics = _get_editable_thematic_ids(user) if is_edit_check else _get_accessible_thematic_ids(user)
|
||||||
|
|
||||||
thematic_folder_ids: set[int] = set()
|
thematic_folder_ids: set[int] = set()
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,8 @@ class DocumentManagementService:
|
||||||
description=None,
|
description=None,
|
||||||
content_object=None,
|
content_object=None,
|
||||||
target_folder=None,
|
target_folder=None,
|
||||||
unique_params=None
|
unique_params=None,
|
||||||
|
visibility=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Ajoute un document avec la logique de versioning appropriée.
|
Ajoute un document avec la logique de versioning appropriée.
|
||||||
|
|
@ -276,6 +277,7 @@ class DocumentManagementService:
|
||||||
détecté automatiquement depuis l'extension du fichier.
|
détecté automatiquement depuis l'extension du fichier.
|
||||||
Le format est TOUJOURS inclus (ex: 'pdf', 'dxf', 'dwg').
|
Le format est TOUJOURS inclus (ex: 'pdf', 'dxf', 'dwg').
|
||||||
L'unicité est donc liée au tag + format (+ paramètres extra).
|
L'unicité est donc liée au tag + format (+ paramètres extra).
|
||||||
|
visibility: VisibilityScope - Niveau de visibilité (optionnel, défaut SCOPED pour les projets)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: {
|
dict: {
|
||||||
|
|
@ -286,7 +288,7 @@ class DocumentManagementService:
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from .models import DocumentAttachment
|
from .models import DocumentAttachment, VisibilityScope
|
||||||
|
|
||||||
# Déterminer le répertoire cible
|
# Déterminer le répertoire cible
|
||||||
if target_folder is None:
|
if target_folder is None:
|
||||||
|
|
@ -329,6 +331,17 @@ class DocumentManagementService:
|
||||||
"message": _("New version added to existing document."),
|
"message": _("New version added to existing document."),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
# Déterminer la visibilité
|
||||||
|
if visibility is None:
|
||||||
|
if content_object and getattr(getattr(content_object, '_meta', None), 'model_name', None) == 'project':
|
||||||
|
doc_visibility = VisibilityScope.SCOPED
|
||||||
|
elif target_folder and getattr(target_folder, 'visibility', None) == VisibilityScope.SCOPED:
|
||||||
|
doc_visibility = VisibilityScope.SCOPED
|
||||||
|
else:
|
||||||
|
doc_visibility = VisibilityScope.INTERNAL
|
||||||
|
else:
|
||||||
|
doc_visibility = visibility
|
||||||
|
|
||||||
# Créer un nouveau document
|
# Créer un nouveau document
|
||||||
doc_title = title or (file.name if hasattr(file, "name") else _("Document"))
|
doc_title = title or (file.name if hasattr(file, "name") else _("Document"))
|
||||||
|
|
||||||
|
|
@ -337,6 +350,7 @@ class DocumentManagementService:
|
||||||
description=description or "",
|
description=description or "",
|
||||||
created_by=user,
|
created_by=user,
|
||||||
unique_params=normalized_params,
|
unique_params=normalized_params,
|
||||||
|
visibility=doc_visibility,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ajouter le tag
|
# Ajouter le tag
|
||||||
|
|
|
||||||
|
|
@ -1206,5 +1206,161 @@ class DocumentVisibilityAndIndependenceTests(TestCase):
|
||||||
self.assertNotIn(child.pk, ids_ex_f)
|
self.assertNotIn(child.pk, ids_ex_f)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectDocumentsVisibilityTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from common.models import UserConfig, Thematic, UserThematics
|
||||||
|
from contracts.models import Company, Team, CompanyMember
|
||||||
|
from projects.models import Project, ProjectUserAccess, ProjectTeamAccess
|
||||||
|
from documents.models import (
|
||||||
|
DocumentAttachment,
|
||||||
|
DocumentFolder,
|
||||||
|
DocumentFolderAttachment,
|
||||||
|
ManagedDocument,
|
||||||
|
VisibilityScope,
|
||||||
|
)
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
self.thematic = Thematic.objects.create(name="Voirie", code="VOI")
|
||||||
|
|
||||||
|
# Internal user
|
||||||
|
self.int_user = User.objects.create_user(username="intern_user", password="pwd")
|
||||||
|
self.int_config = UserConfig.objects.create(user=self.int_user, is_intern=True)
|
||||||
|
UserThematics.objects.create(user_config=self.int_config, thematic=self.thematic, can_view_projects=True)
|
||||||
|
|
||||||
|
# External user 1 (direct user access)
|
||||||
|
self.ext_user = User.objects.create_user(username="ext_user", password="pwd")
|
||||||
|
self.ext_config = UserConfig.objects.create(user=self.ext_user, is_intern=False)
|
||||||
|
|
||||||
|
# External user 2 (team access)
|
||||||
|
self.ext_team_user = User.objects.create_user(username="ext_team_user", password="pwd")
|
||||||
|
self.ext_team_config = UserConfig.objects.create(user=self.ext_team_user, is_intern=False)
|
||||||
|
self.company = Company.objects.create(name="Contractor Corp")
|
||||||
|
self.team = Team.objects.create(name="Project Team", company=self.company)
|
||||||
|
CompanyMember.objects.create(user=self.ext_team_user, company=self.company, team=self.team)
|
||||||
|
|
||||||
|
# External user 3 (no access)
|
||||||
|
self.ext_no_access = User.objects.create_user(username="ext_no_access", password="pwd")
|
||||||
|
self.ext_no_access_config = UserConfig.objects.create(user=self.ext_no_access, is_intern=False)
|
||||||
|
|
||||||
|
# Project
|
||||||
|
self.project = Project.objects.create(
|
||||||
|
code="P12345",
|
||||||
|
name="Boulevard Project",
|
||||||
|
created_by=self.int_user,
|
||||||
|
)
|
||||||
|
self.project.thematics.add(self.thematic)
|
||||||
|
|
||||||
|
# Grant direct access to ext_user
|
||||||
|
ProjectUserAccess.objects.create(project=self.project, user=self.ext_user, can_view=True)
|
||||||
|
|
||||||
|
# Grant team access to team
|
||||||
|
ProjectTeamAccess.objects.create(project=self.project, team=self.team, can_view=True)
|
||||||
|
|
||||||
|
# Create Project Folder (SCOPED)
|
||||||
|
self.project_folder = DocumentFolder.objects.create(
|
||||||
|
name="P12345 Documents",
|
||||||
|
slug="project-p12345-boulevard",
|
||||||
|
application_label="projects",
|
||||||
|
visibility=VisibilityScope.SCOPED,
|
||||||
|
created_by=self.int_user,
|
||||||
|
)
|
||||||
|
project_ct = ContentType.objects.get_for_model(self.project)
|
||||||
|
DocumentFolderAttachment.objects.create(
|
||||||
|
folder=self.project_folder,
|
||||||
|
content_type=project_ct,
|
||||||
|
object_id=self.project.pk,
|
||||||
|
is_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create Scoped Document
|
||||||
|
self.scoped_doc = ManagedDocument.objects.create(
|
||||||
|
title="Plan Execution",
|
||||||
|
visibility=VisibilityScope.SCOPED,
|
||||||
|
created_by=self.int_user,
|
||||||
|
)
|
||||||
|
self.scoped_doc.folders.add(self.project_folder)
|
||||||
|
DocumentAttachment.objects.create(
|
||||||
|
document=self.scoped_doc,
|
||||||
|
content_type=project_ct,
|
||||||
|
object_id=self.project.pk,
|
||||||
|
attached_by=self.int_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create Internal Document
|
||||||
|
self.internal_doc = ManagedDocument.objects.create(
|
||||||
|
title="Internal Budget",
|
||||||
|
visibility=VisibilityScope.INTERNAL,
|
||||||
|
created_by=self.int_user,
|
||||||
|
)
|
||||||
|
self.internal_doc.folders.add(self.project_folder)
|
||||||
|
DocumentAttachment.objects.create(
|
||||||
|
document=self.internal_doc,
|
||||||
|
content_type=project_ct,
|
||||||
|
object_id=self.project.pk,
|
||||||
|
attached_by=self.int_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_external_user_can_view_scoped_project_documents_and_folders(self):
|
||||||
|
from .permissions import (
|
||||||
|
filter_documents_for_user,
|
||||||
|
filter_folders_for_user,
|
||||||
|
user_has_document_permission,
|
||||||
|
user_has_folder_permission,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(user_has_document_permission(self.ext_user, self.scoped_doc))
|
||||||
|
self.assertTrue(user_has_folder_permission(self.ext_user, self.project_folder))
|
||||||
|
|
||||||
|
doc_pks = set(filter_documents_for_user(ManagedDocument.objects.all(), self.ext_user).values_list("pk", flat=True))
|
||||||
|
folder_pks = set(filter_folders_for_user(DocumentFolder.objects.all(), self.ext_user).values_list("pk", flat=True))
|
||||||
|
|
||||||
|
self.assertIn(self.scoped_doc.pk, doc_pks)
|
||||||
|
self.assertIn(self.project_folder.pk, folder_pks)
|
||||||
|
|
||||||
|
def test_external_user_cannot_view_internal_project_documents(self):
|
||||||
|
from .permissions import (
|
||||||
|
filter_documents_for_user,
|
||||||
|
user_has_document_permission,
|
||||||
|
)
|
||||||
|
|
||||||
|
# External user cannot view internal doc
|
||||||
|
self.assertFalse(user_has_document_permission(self.ext_user, self.internal_doc))
|
||||||
|
ext_doc_pks = set(filter_documents_for_user(ManagedDocument.objects.all(), self.ext_user).values_list("pk", flat=True))
|
||||||
|
self.assertNotIn(self.internal_doc.pk, ext_doc_pks)
|
||||||
|
|
||||||
|
# Internal user CAN view internal doc
|
||||||
|
self.assertTrue(user_has_document_permission(self.int_user, self.internal_doc))
|
||||||
|
int_doc_pks = set(filter_documents_for_user(ManagedDocument.objects.all(), self.int_user).values_list("pk", flat=True))
|
||||||
|
self.assertIn(self.internal_doc.pk, int_doc_pks)
|
||||||
|
|
||||||
|
def test_external_user_via_team_access_can_view_scoped_project_documents(self):
|
||||||
|
from .permissions import (
|
||||||
|
filter_documents_for_user,
|
||||||
|
user_has_document_permission,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(user_has_document_permission(self.ext_team_user, self.scoped_doc))
|
||||||
|
team_doc_pks = set(filter_documents_for_user(ManagedDocument.objects.all(), self.ext_team_user).values_list("pk", flat=True))
|
||||||
|
self.assertIn(self.scoped_doc.pk, team_doc_pks)
|
||||||
|
self.assertNotIn(self.internal_doc.pk, team_doc_pks)
|
||||||
|
|
||||||
|
def test_external_user_without_project_access_cannot_view_project_documents(self):
|
||||||
|
from .permissions import (
|
||||||
|
filter_documents_for_user,
|
||||||
|
user_has_document_permission,
|
||||||
|
user_has_folder_permission,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(user_has_document_permission(self.ext_no_access, self.scoped_doc))
|
||||||
|
self.assertFalse(user_has_folder_permission(self.ext_no_access, self.project_folder))
|
||||||
|
|
||||||
|
no_acc_doc_pks = set(filter_documents_for_user(ManagedDocument.objects.all(), self.ext_no_access).values_list("pk", flat=True))
|
||||||
|
no_acc_folder_pks = set(filter_folders_for_user(DocumentFolder.objects.all(), self.ext_no_access).values_list("pk", flat=True))
|
||||||
|
|
||||||
|
self.assertNotIn(self.scoped_doc.pk, no_acc_doc_pks)
|
||||||
|
self.assertNotIn(self.project_folder.pk, no_acc_folder_pks)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -183,9 +183,12 @@ class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
title = os.path.splitext(proj_doc.filename)[0] or proj_doc.filename
|
title = os.path.splitext(proj_doc.filename)[0] or proj_doc.filename
|
||||||
|
|
||||||
|
from documents.models import VisibilityScope
|
||||||
|
|
||||||
managed = ManagedDocument.objects.create(
|
managed = ManagedDocument.objects.create(
|
||||||
title=title,
|
title=title,
|
||||||
created_by=proj_doc.uploaded_by,
|
created_by=proj_doc.uploaded_by,
|
||||||
|
visibility=VisibilityScope.SCOPED,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build a DocumentVersion referencing the existing file path directly.
|
# Build a DocumentVersion referencing the existing file path directly.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def make_project_documents_and_folders_scoped(apps, schema_editor):
|
||||||
|
DocumentFolder = apps.get_model('documents', 'DocumentFolder')
|
||||||
|
ManagedDocument = apps.get_model('documents', 'ManagedDocument')
|
||||||
|
DocumentFolderAttachment = apps.get_model('documents', 'DocumentFolderAttachment')
|
||||||
|
DocumentAttachment = apps.get_model('documents', 'DocumentAttachment')
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
|
||||||
|
try:
|
||||||
|
project_ct = ContentType.objects.get(app_label='projects', model='project')
|
||||||
|
except ContentType.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update project folders from internal -> scoped
|
||||||
|
project_folder_ids = list(
|
||||||
|
DocumentFolderAttachment.objects.filter(
|
||||||
|
content_type=project_ct
|
||||||
|
).values_list('folder_id', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if project_folder_ids:
|
||||||
|
DocumentFolder.objects.filter(
|
||||||
|
pk__in=project_folder_ids,
|
||||||
|
visibility='internal'
|
||||||
|
).update(visibility='scoped')
|
||||||
|
|
||||||
|
# Also update folders with application_label='projects'
|
||||||
|
DocumentFolder.objects.filter(
|
||||||
|
application_label='projects',
|
||||||
|
visibility='internal'
|
||||||
|
).update(visibility='scoped')
|
||||||
|
|
||||||
|
# Update project documents from internal -> scoped
|
||||||
|
project_doc_ids = list(
|
||||||
|
DocumentAttachment.objects.filter(
|
||||||
|
content_type=project_ct
|
||||||
|
).values_list('document_id', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if project_doc_ids:
|
||||||
|
ManagedDocument.objects.filter(
|
||||||
|
pk__in=project_doc_ids,
|
||||||
|
visibility='internal'
|
||||||
|
).update(visibility='scoped')
|
||||||
|
|
||||||
|
# Also update documents in project folders
|
||||||
|
if project_folder_ids:
|
||||||
|
ManagedDocument.objects.filter(
|
||||||
|
folders__pk__in=project_folder_ids,
|
||||||
|
visibility='internal'
|
||||||
|
).update(visibility='scoped')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0001_initial'),
|
||||||
|
('documents', '0003_documentfolder_visibility_manageddocument_thematics_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
make_project_documents_and_folders_scoped,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -20,18 +20,19 @@ def ensure_project_document_folder(project):
|
||||||
"""
|
"""
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from documents.models import DocumentFolder, DocumentFolderAttachment
|
from documents.models import DocumentFolder, DocumentFolderAttachment, VisibilityScope
|
||||||
|
|
||||||
parent = DocumentFolder.objects.filter(slug="projects", is_predefined=True).first()
|
parent = DocumentFolder.objects.filter(slug="projects", is_predefined=True).first()
|
||||||
|
|
||||||
folder_name = f"{project.code} \u2013 {project.name[:60]}"
|
folder_name = f"{project.code} \u2013 {project.name[:60]}"
|
||||||
folder_slug = f"project-{project.code.lower()}-{slugify(project.name)[:80]}"
|
folder_slug = f"project-{project.code.lower()}-{slugify(project.name)[:80]}"
|
||||||
|
|
||||||
folder, _ = DocumentFolder.objects.get_or_create(
|
folder, created = DocumentFolder.objects.get_or_create(
|
||||||
slug=folder_slug,
|
slug=folder_slug,
|
||||||
defaults={
|
defaults={
|
||||||
"name": folder_name,
|
"name": folder_name,
|
||||||
"application_label": "projects",
|
"application_label": "projects",
|
||||||
|
"visibility": VisibilityScope.SCOPED,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -569,6 +569,7 @@ let docAvailableFolders = [];
|
||||||
.then(data => {
|
.then(data => {
|
||||||
docConfig.use_managed_documents = data.use_managed_documents;
|
docConfig.use_managed_documents = data.use_managed_documents;
|
||||||
docConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents;
|
docConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents;
|
||||||
|
docConfig.can_change_visibility = data.can_change_visibility;
|
||||||
docAvailableTags = data.tags || [];
|
docAvailableTags = data.tags || [];
|
||||||
docAvailableFolders = data.folders || [];
|
docAvailableFolders = data.folders || [];
|
||||||
})
|
})
|
||||||
|
|
@ -641,6 +642,32 @@ function renderDocFileFields() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Visibility select ──
|
||||||
|
if (docConfig.can_change_visibility) {
|
||||||
|
const visSel = document.createElement('select');
|
||||||
|
visSel.name = `visibility_${index}`;
|
||||||
|
visSel.classList.add('form-select', 'w-auto');
|
||||||
|
visSel.style.minWidth = '160px';
|
||||||
|
|
||||||
|
const optScoped = document.createElement('option');
|
||||||
|
optScoped.value = 'scoped';
|
||||||
|
optScoped.textContent = gettext('Externe & Interne');
|
||||||
|
optScoped.selected = true;
|
||||||
|
visSel.appendChild(optScoped);
|
||||||
|
|
||||||
|
const optInternal = document.createElement('option');
|
||||||
|
optInternal.value = 'internal';
|
||||||
|
optInternal.textContent = gettext('Interne uniquement');
|
||||||
|
visSel.appendChild(optInternal);
|
||||||
|
|
||||||
|
const optPrivate = document.createElement('option');
|
||||||
|
optPrivate.value = 'private';
|
||||||
|
optPrivate.textContent = gettext('Privé');
|
||||||
|
visSel.appendChild(optPrivate);
|
||||||
|
|
||||||
|
row.appendChild(visSel);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// ── Legacy: hidden input keeps count consistent ──
|
// ── Legacy: hidden input keeps count consistent ──
|
||||||
const hidden = document.createElement('input');
|
const hidden = document.createElement('input');
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@ let docAvailableFolders = [];
|
||||||
.then(data => {
|
.then(data => {
|
||||||
docConfig.use_managed_documents = data.use_managed_documents;
|
docConfig.use_managed_documents = data.use_managed_documents;
|
||||||
docConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents;
|
docConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents;
|
||||||
|
docConfig.can_change_visibility = data.can_change_visibility;
|
||||||
docAvailableTags = data.tags || [];
|
docAvailableTags = data.tags || [];
|
||||||
docAvailableFolders = data.folders || [];
|
docAvailableFolders = data.folders || [];
|
||||||
})
|
})
|
||||||
|
|
@ -259,6 +260,30 @@ function renderDocFileFields() {
|
||||||
tagSel.appendChild(opt);
|
tagSel.appendChild(opt);
|
||||||
});
|
});
|
||||||
row.appendChild(tagSel);
|
row.appendChild(tagSel);
|
||||||
|
|
||||||
|
if (docConfig.can_change_visibility) {
|
||||||
|
const visSel = document.createElement('select');
|
||||||
|
visSel.name = `visibility_${index}`;
|
||||||
|
visSel.classList.add('form-select', 'form-select-sm', 'w-auto');
|
||||||
|
|
||||||
|
const optScoped = document.createElement('option');
|
||||||
|
optScoped.value = 'scoped';
|
||||||
|
optScoped.textContent = gettext('Externe & Interne');
|
||||||
|
optScoped.selected = true;
|
||||||
|
visSel.appendChild(optScoped);
|
||||||
|
|
||||||
|
const optInternal = document.createElement('option');
|
||||||
|
optInternal.value = 'internal';
|
||||||
|
optInternal.textContent = gettext('Interne uniquement');
|
||||||
|
visSel.appendChild(optInternal);
|
||||||
|
|
||||||
|
const optPrivate = document.createElement('option');
|
||||||
|
optPrivate.value = 'private';
|
||||||
|
optPrivate.textContent = gettext('Privé');
|
||||||
|
visSel.appendChild(optPrivate);
|
||||||
|
|
||||||
|
row.appendChild(visSel);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const hidden = document.createElement('input');
|
const hidden = document.createElement('input');
|
||||||
hidden.type = 'hidden';
|
hidden.type = 'hidden';
|
||||||
|
|
|
||||||
|
|
@ -590,6 +590,19 @@ def projects_add_document(request, project_id):
|
||||||
if unique_param_value:
|
if unique_param_value:
|
||||||
unique_params = {'cables': unique_param_value}
|
unique_params = {'cables': unique_param_value}
|
||||||
|
|
||||||
|
# Visibilité
|
||||||
|
from documents.models import VisibilityScope
|
||||||
|
from documents.permissions import user_can_change_visibility
|
||||||
|
raw_visibility = request.POST.get(f'visibility_{i}', '').strip()
|
||||||
|
visibility = None
|
||||||
|
if raw_visibility and raw_visibility in dict(VisibilityScope.CHOICES):
|
||||||
|
if user_can_change_visibility(request.user):
|
||||||
|
visibility = raw_visibility
|
||||||
|
else:
|
||||||
|
visibility = VisibilityScope.SCOPED
|
||||||
|
else:
|
||||||
|
visibility = VisibilityScope.SCOPED
|
||||||
|
|
||||||
DocumentManagementService.add_document(
|
DocumentManagementService.add_document(
|
||||||
file=file,
|
file=file,
|
||||||
tag=tag,
|
tag=tag,
|
||||||
|
|
@ -598,6 +611,7 @@ def projects_add_document(request, project_id):
|
||||||
content_object=project,
|
content_object=project,
|
||||||
target_folder=target_folder,
|
target_folder=target_folder,
|
||||||
unique_params=unique_params,
|
unique_params=unique_params,
|
||||||
|
visibility=visibility,
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, _("Document(s) sauvegardé(s) avec succès !"))
|
messages.success(request, _("Document(s) sauvegardé(s) avec succès !"))
|
||||||
|
|
@ -905,9 +919,13 @@ def projects_document_config(request, project_id):
|
||||||
|
|
||||||
folders = DocumentManagementService.get_subfolders_for_object(project)
|
folders = DocumentManagementService.get_subfolders_for_object(project)
|
||||||
|
|
||||||
|
from documents.permissions import user_can_change_visibility
|
||||||
|
can_change_vis = user_can_change_visibility(user)
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'use_managed_documents': True,
|
'use_managed_documents': True,
|
||||||
'can_add_unique_tag_documents': can_add_unique,
|
'can_add_unique_tag_documents': can_add_unique,
|
||||||
|
'can_change_visibility': can_change_vis,
|
||||||
'tags': tags_data,
|
'tags': tags_data,
|
||||||
'folders': folders,
|
'folders': folders,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue