diff --git a/loko/documents/forms.py b/loko/documents/forms.py
index ccb1615..256d74c 100644
--- a/loko/documents/forms.py
+++ b/loko/documents/forms.py
@@ -66,15 +66,17 @@ class DocumentUpdateForm(forms.ModelForm):
self.fields["folders"].widget.attrs.setdefault("class", "form-select")
self.fields["folders"].required = False
if self.user:
- accessible_folders = filter_folders_for_user(
- DocumentFolder.objects.prefetch_related("parent_folders"),
+ self.fields["folders"].queryset = filter_folders_for_user(
+ DocumentFolder.objects.all(),
self.user,
required_permission=ManagedDocument.PERMISSION_VIEW,
)
- sorted_folders = sorted(accessible_folders, key=lambda f: f.get_full_path().lower())
- self.fields["folders"].queryset = accessible_folders
- self.fields["folders"].choices = [(f.pk, f.get_full_path()) for f in sorted_folders]
self.fields["folders"].label_from_instance = lambda obj: obj.get_full_path()
+ if self.instance and self.instance.pk:
+ current_folders = list(self.instance.folders.all().prefetch_related("parent_folders"))
+ self.fields["folders"].choices = [(f.pk, f.get_full_path()) for f in current_folders]
+ else:
+ self.fields["folders"].choices = []
def clean_visibility(self):
if self.user and not user_can_change_visibility(self.user):
@@ -111,15 +113,19 @@ class ManagedDocumentForm(forms.ModelForm):
# Limiter les folders accessibles à l'utilisateur
if user:
- accessible_folders = filter_folders_for_user(
- DocumentFolder.objects.prefetch_related("parent_folders"),
+ self.fields["folders"].queryset = filter_folders_for_user(
+ DocumentFolder.objects.all(),
user,
required_permission=ManagedDocument.PERMISSION_VIEW
)
- sorted_folders = sorted(accessible_folders, key=lambda f: f.get_full_path().lower())
- self.fields["folders"].queryset = accessible_folders
- self.fields["folders"].choices = [(f.pk, f.get_full_path()) for f in sorted_folders]
self.fields["folders"].label_from_instance = lambda obj: obj.get_full_path()
+ if initial_folder:
+ self.fields["folders"].choices = [(initial_folder.pk, initial_folder.get_full_path())]
+ elif self.instance and self.instance.pk:
+ current_folders = list(self.instance.folders.all().prefetch_related("parent_folders"))
+ self.fields["folders"].choices = [(f.pk, f.get_full_path()) for f in current_folders]
+ else:
+ self.fields["folders"].choices = []
# Pré-sélectionner le folder initial si fourni
if initial_folder and not self.instance.pk:
@@ -480,14 +486,13 @@ class DocumentFolderForm(forms.ModelForm):
instance = getattr(self, "instance", None)
if instance and instance.pk:
excluded_ids = {instance.pk, *self._get_descendant_ids(instance)}
- parent_qs = (
- DocumentFolder.objects.prefetch_related("parent_folders").exclude(pk__in=excluded_ids)
- )
+ parent_qs = DocumentFolder.objects.exclude(pk__in=excluded_ids)
+ current_parents = list(instance.parent_folders.all().prefetch_related("parent_folders"))
+ self.fields["parent_folders"].choices = [(f.pk, f.get_full_path()) for f in current_parents]
else:
- parent_qs = DocumentFolder.objects.prefetch_related("parent_folders")
- sorted_parents = sorted(parent_qs, key=lambda f: f.get_full_path().lower())
+ parent_qs = DocumentFolder.objects.all()
+ self.fields["parent_folders"].choices = []
self.fields["parent_folders"].queryset = parent_qs
- self.fields["parent_folders"].choices = [(f.pk, f.get_full_path()) for f in sorted_parents]
self.fields["parent_folders"].label_from_instance = lambda obj: obj.get_full_path()
if "thematics" in self.fields:
diff --git a/loko/documents/templates/documents/document_detail.html b/loko/documents/templates/documents/document_detail.html
index 9d792b2..500d8ab 100644
--- a/loko/documents/templates/documents/document_detail.html
+++ b/loko/documents/templates/documents/document_detail.html
@@ -129,9 +129,6 @@
@@ -760,8 +757,8 @@ document.addEventListener('DOMContentLoaded', function () {
@@ -794,13 +791,32 @@ document.addEventListener('DOMContentLoaded', function () {
$(document).ready(function() {
// Select2 for document folders in modal
$('#editDocumentModal').on('shown.bs.modal', function() {
- $('#id_doc_folders').select2({
- dropdownParent: $('#editDocumentModal'),
- theme: 'bootstrap-5',
- placeholder: "{% translate 'Rechercher des répertoires...' %}",
- allowClear: true,
- width: '100%'
- });
+ if (!$('#id_doc_folders').hasClass('select2-hidden-accessible')) {
+ $('#id_doc_folders').select2({
+ dropdownParent: $('#editDocumentModal'),
+ theme: 'bootstrap-5',
+ placeholder: "{% translate 'Rechercher des répertoires...' %}",
+ allowClear: true,
+ width: '100%',
+ ajax: {
+ url: "{% url 'documents:folder_autocomplete' %}",
+ dataType: 'json',
+ delay: 250,
+ data: function (params) {
+ return {
+ q: params.term || ''
+ };
+ },
+ processResults: function (data) {
+ return {
+ results: data.results
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 0
+ });
+ }
});
// Select2 for add folder select in card
@@ -809,7 +825,25 @@ document.addEventListener('DOMContentLoaded', function () {
theme: 'bootstrap-5',
placeholder: "{% translate 'Sélectionner un répertoire...' %}",
allowClear: true,
- width: '100%'
+ width: '100%',
+ ajax: {
+ url: "{% url 'documents:folder_autocomplete' %}",
+ dataType: 'json',
+ delay: 250,
+ data: function (params) {
+ return {
+ q: params.term || '',
+ exclude_doc: "{{ document.pk }}"
+ };
+ },
+ processResults: function (data) {
+ return {
+ results: data.results
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 0
});
}
diff --git a/loko/documents/templates/documents/document_form.html b/loko/documents/templates/documents/document_form.html
index 997297f..9b091ab 100644
--- a/loko/documents/templates/documents/document_form.html
+++ b/loko/documents/templates/documents/document_form.html
@@ -114,7 +114,24 @@
theme: 'bootstrap-5',
placeholder: "{% translate 'Rechercher des répertoires...' %}",
allowClear: true,
- width: '100%'
+ width: '100%',
+ ajax: {
+ url: "{% url 'documents:folder_autocomplete' %}",
+ dataType: 'json',
+ delay: 250,
+ data: function (params) {
+ return {
+ q: params.term || ''
+ };
+ },
+ processResults: function (data) {
+ return {
+ results: data.results
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 0
});
}
if ($('#id_thematics').length) {
diff --git a/loko/documents/templates/documents/documentfolder_form.html b/loko/documents/templates/documents/documentfolder_form.html
index 652cb82..a4017c5 100644
--- a/loko/documents/templates/documents/documentfolder_form.html
+++ b/loko/documents/templates/documents/documentfolder_form.html
@@ -87,7 +87,25 @@
theme: 'bootstrap-5',
placeholder: "{% translate 'Rechercher des répertoires parents...' %}",
allowClear: true,
- width: '100%'
+ width: '100%',
+ ajax: {
+ url: "{% url 'documents:folder_autocomplete' %}",
+ dataType: 'json',
+ delay: 250,
+ data: function (params) {
+ return {
+ q: params.term || '',
+ exclude_folder: "{{ folder.pk|default:'' }}"
+ };
+ },
+ processResults: function (data) {
+ return {
+ results: data.results
+ };
+ },
+ cache: true
+ },
+ minimumInputLength: 0
});
}
if ($('#id_thematics').length) {
diff --git a/loko/documents/tests.py b/loko/documents/tests.py
index 6ca5998..a8c6893 100644
--- a/loko/documents/tests.py
+++ b/loko/documents/tests.py
@@ -1090,14 +1090,16 @@ class DocumentVisibilityAndIndependenceTests(TestCase):
self.assertEqual(str(asbuilt), "Anderlecht / Carrefour 1 / As-Built")
# Test ManagedDocumentForm choices
- form = ManagedDocumentForm(user=self.creator)
- folder_choices = dict(form.fields["folders"].choices)
+ form_initial = ManagedDocumentForm(user=self.creator, initial_folder=asbuilt)
+ folder_choices = dict(form_initial.fields["folders"].choices)
self.assertEqual(folder_choices[asbuilt.pk], "Anderlecht / Carrefour 1 / As-Built")
+ self.assertEqual(form_initial.fields["folders"].label_from_instance(asbuilt), "Anderlecht / Carrefour 1 / As-Built")
- # Test DocumentFolderForm choices
- folder_form = DocumentFolderForm(user=self.creator)
+ # Test DocumentFolderForm choices for bound instance
+ folder_form = DocumentFolderForm(user=self.creator, instance=asbuilt)
parent_choices = dict(folder_form.fields["parent_folders"].choices)
self.assertEqual(parent_choices[carrefour.pk], "Anderlecht / Carrefour 1")
+ self.assertEqual(folder_form.fields["parent_folders"].label_from_instance(carrefour), "Anderlecht / Carrefour 1")
def test_user_display_name_with_company(self):
from common.utils import get_user_display_name
@@ -1159,6 +1161,50 @@ class DocumentVisibilityAndIndependenceTests(TestCase):
self.assertEqual(doc.shares.get(user=u1).permission, ManagedDocument.PERMISSION_EDIT)
self.assertEqual(doc.shares.get(user=u2).permission, ManagedDocument.PERMISSION_EDIT)
+ def test_folder_autocomplete_view(self):
+ from .models import VisibilityScope
+ self.client.force_login(self.creator)
+ parent = DocumentFolder.objects.create(name="Parent Dir", created_by=self.creator)
+ child = DocumentFolder.objects.create(name="Child Dir", created_by=self.creator)
+ child.parent_folders.add(parent)
+
+ doc = ManagedDocument.objects.create(
+ title="Doc In Child",
+ created_by=self.creator,
+ visibility=VisibilityScope.INTERNAL,
+ )
+ doc.folders.add(child)
+
+ # Autocomplete without filter
+ url = reverse("documents:folder_autocomplete")
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+ data = res.json()
+ self.assertIn("results", data)
+ texts = [item["text"] for item in data["results"]]
+ self.assertTrue(any("Parent Dir / Child Dir" in t for t in texts))
+
+ # Search query matching child
+ res_q = self.client.get(url, {"q": "Child"})
+ data_q = res_q.json()
+ self.assertEqual(len(data_q["results"]), 1)
+ self.assertEqual(data_q["results"][0]["id"], child.pk)
+ self.assertEqual(data_q["results"][0]["text"], "Parent Dir / Child Dir")
+
+ # Exclude doc
+ res_ex_doc = self.client.get(url, {"exclude_doc": doc.pk})
+ data_ex_doc = res_ex_doc.json()
+ ids_ex = [item["id"] for item in data_ex_doc["results"]]
+ self.assertNotIn(child.pk, ids_ex)
+ self.assertIn(parent.pk, ids_ex)
+
+ # Exclude folder (parent excludes child if excluding parent or child excludes descendants)
+ res_ex_folder = self.client.get(url, {"exclude_folder": parent.pk})
+ data_ex_folder = res_ex_folder.json()
+ ids_ex_f = [item["id"] for item in data_ex_folder["results"]]
+ self.assertNotIn(parent.pk, ids_ex_f)
+ self.assertNotIn(child.pk, ids_ex_f)
+
\ No newline at end of file
diff --git a/loko/documents/urls.py b/loko/documents/urls.py
index 6903a09..7833cf1 100644
--- a/loko/documents/urls.py
+++ b/loko/documents/urls.py
@@ -7,6 +7,7 @@ app_name = "documents"
urlpatterns = [
path("", views.DocumentListView.as_view(), name="list"),
+ path("folders/autocomplete/", views.FolderAutocompleteView.as_view(), name="folder_autocomplete"),
path("create/", views.DocumentCreateView.as_view(), name="create"),
path("/delete/", views.DocumentDeleteView.as_view(), name="delete"),
path("folders/create/", views.DocumentFolderCreateView.as_view(), name="folder_create"),
diff --git a/loko/documents/views.py b/loko/documents/views.py
index a2adb94..eff2f37 100644
--- a/loko/documents/views.py
+++ b/loko/documents/views.py
@@ -1095,22 +1095,6 @@ class DocumentDetailView(DocumentsAppAccessMixin, LoginRequiredMixin, DetailView
context["can_share_document"] = user_can_share(self.request.user) and can_edit
context["can_change_visibility"] = user_can_change_visibility(self.request.user)
- # Available folders/tags for add forms (exclude already associated ones)
- existing_folder_ids = list(document.folders.values_list("pk", flat=True))
- accessible_folders = filter_folders_for_user(
- DocumentFolder.objects.prefetch_related("parent_folders"),
- self.request.user,
- required_permission=ManagedDocument.PERMISSION_VIEW,
- )
- context["available_folders"] = sorted(
- accessible_folders.exclude(pk__in=existing_folder_ids),
- key=lambda f: f.get_full_path().lower(),
- )
- context["all_accessible_folders"] = sorted(
- accessible_folders,
- key=lambda f: f.get_full_path().lower(),
- )
-
existing_tag_ids = list(document.tags.values_list("pk", flat=True))
context["available_tags"] = DocumentTag.objects.exclude(
pk__in=existing_tag_ids
@@ -1567,6 +1551,62 @@ class SearchObjectsView(LoginRequiredMixin, View):
return JsonResponse({"results": results})
+class FolderAutocompleteView(DocumentsAppAccessMixin, LoginRequiredMixin, View):
+ def get(self, request):
+ from django.db.models import Q
+
+ q = request.GET.get("q", "").strip()
+ exclude_doc_id = request.GET.get("exclude_doc")
+ exclude_folder_id = request.GET.get("exclude_folder")
+
+ queryset = filter_folders_for_user(
+ DocumentFolder.objects.all(),
+ request.user,
+ required_permission=ManagedDocument.PERMISSION_VIEW,
+ )
+
+ if exclude_doc_id:
+ try:
+ doc = ManagedDocument.objects.get(pk=int(exclude_doc_id))
+ existing_pks = doc.folders.values_list("pk", flat=True)
+ queryset = queryset.exclude(pk__in=existing_pks)
+ except (ManagedDocument.DoesNotExist, ValueError):
+ pass
+
+ if exclude_folder_id:
+ try:
+ folder = DocumentFolder.objects.get(pk=int(exclude_folder_id))
+ descendant_ids = set()
+ stack = [folder.pk]
+ while stack:
+ curr = stack.pop()
+ if curr in descendant_ids:
+ continue
+ descendant_ids.add(curr)
+ child_ids = DocumentFolder.objects.filter(parent_folders__pk=curr).values_list("pk", flat=True)
+ stack.extend(child_ids)
+ queryset = queryset.exclude(pk__in=descendant_ids)
+ except (DocumentFolder.DoesNotExist, ValueError):
+ pass
+
+ if q:
+ queryset = queryset.filter(
+ Q(name__icontains=q) | Q(slug__icontains=q) | Q(parent_folders__name__icontains=q)
+ ).distinct()
+
+ queryset = queryset.prefetch_related("parent_folders")[:100]
+
+ results = []
+ for folder in queryset:
+ results.append({
+ "id": folder.pk,
+ "text": folder.get_full_path(),
+ })
+
+ results.sort(key=lambda item: item["text"].lower())
+ return JsonResponse({"results": results})
+
+
from pdf2image import convert_from_path, pdfinfo_from_path
from io import BytesIO
from django.core.cache import cache