refactor: optimize document navigation, add model database indexes, and improve as-built document import logic

This commit is contained in:
kdeterme 2026-08-21 17:17:30 +02:00
parent 5fd6ded07f
commit ce48b99a45
4 changed files with 86 additions and 43 deletions

View file

@ -1,4 +1,5 @@
import os import os
import shutil
import subprocess import subprocess
import tempfile import tempfile
import zipfile import zipfile
@ -85,6 +86,8 @@ class Command(BaseCommand):
if not os.path.isfile(source_zip): if not os.path.isfile(source_zip):
raise CommandError(f"ZIP file '{source_zip}' does not exist.") raise CommandError(f"ZIP file '{source_zip}' does not exist.")
self.stdout.write(self.style.NOTICE(f"Extracting ZIP archive '{source_zip}' into '{source_dir}'...")) self.stdout.write(self.style.NOTICE(f"Extracting ZIP archive '{source_zip}' into '{source_dir}'..."))
if os.path.exists(source_dir):
shutil.rmtree(source_dir, ignore_errors=True)
os.makedirs(source_dir, exist_ok=True) os.makedirs(source_dir, exist_ok=True)
with zipfile.ZipFile(source_zip, 'r') as zip_ref: with zipfile.ZipFile(source_zip, 'r') as zip_ref:
zip_ref.extractall(source_dir) zip_ref.extractall(source_dir)
@ -244,6 +247,23 @@ class Command(BaseCommand):
struct_dir = p struct_dir = p
break break
if not struct_dir and os.path.isdir(source_dir):
# Check if any subfolder name contains structure code or main keywords from name_fr
name_words = [w.lower() for w in (structure.name_fr or "").replace("-", " ").replace("_", " ").replace("(", "").replace(")", "").split() if len(w) > 3 and w.lower() not in ["tunnel", "vers", "pont", "viaduc"]]
for folder_item in os.listdir(source_dir):
full_p = os.path.join(source_dir, folder_item)
if os.path.isdir(full_p) and not folder_item.startswith("."):
folder_lower = folder_item.lower()
if structure.code.lower() in folder_lower or any(w in folder_lower for w in name_words):
struct_dir = full_p
break
# If still not found and single structure requested with single directory extracted
if not struct_dir and target_codes and len(target_codes) == 1:
subdirs = [os.path.join(source_dir, d) for d in os.listdir(source_dir) if os.path.isdir(os.path.join(source_dir, d)) and not d.startswith(".")]
if len(subdirs) == 1:
struct_dir = subdirs[0]
# Collect files with their relative subfolder paths # Collect files with their relative subfolder paths
file_entries = [] # tuples of (full_file_path, relative_dir_path) file_entries = [] # tuples of (full_file_path, relative_dir_path)
if struct_dir: if struct_dir:
@ -282,6 +302,15 @@ class Command(BaseCommand):
if not dry_run: if not dry_run:
existing_docs.delete() existing_docs.delete()
descendant_folder_ids = [f_id for f_id in all_ab_folders if f_id != as_built_folder.pk]
if descendant_folder_ids:
descendant_folders = DocumentFolder.objects.filter(pk__in=descendant_folder_ids)
f_count = descendant_folders.count()
if f_count > 0:
self.stdout.write(self.style.WARNING(f" Cleaning {f_count} existing subfolder(s) in As-Built folder..."))
if not dry_run:
descendant_folders.delete()
self.stdout.write(f" Target root subfolder: Electromechanics / As-Built (ID: {as_built_folder.pk or 'dry-run'})") self.stdout.write(f" Target root subfolder: Electromechanics / As-Built (ID: {as_built_folder.pk or 'dry-run'})")
self.stdout.write(f" Found {len(file_entries)} file(s) to process.") self.stdout.write(f" Found {len(file_entries)} file(s) to process.")

View file

@ -0,0 +1,37 @@
# Generated by Django 6.0.7 on 2026-08-21 14:42
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0013_userconfig_exceptional_transport_can_edit_and_more'),
('contenttypes', '0002_remove_content_type_name'),
('documents', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name='documentattachment',
index=models.Index(fields=['content_type', 'object_id'], name='documents_d_content_5921e5_idx'),
),
migrations.AddIndex(
model_name='documentfolder',
index=models.Index(fields=['name'], name='documents_d_name_aa9319_idx'),
),
migrations.AddIndex(
model_name='documentfolder',
index=models.Index(fields=['slug'], name='documents_d_slug_9cd839_idx'),
),
migrations.AddIndex(
model_name='manageddocument',
index=models.Index(fields=['is_archived', '-updated_at'], name='documents_m_is_arch_8d314f_idx'),
),
migrations.AddIndex(
model_name='manageddocument',
index=models.Index(fields=['title'], name='documents_m_title_b587a8_idx'),
),
]

View file

@ -148,6 +148,10 @@ class DocumentFolder(models.Model):
ordering = ["name"] ordering = ["name"]
verbose_name = _("Document folder") verbose_name = _("Document folder")
verbose_name_plural = _("Document folders") verbose_name_plural = _("Document folders")
indexes = [
models.Index(fields=["name"]),
models.Index(fields=["slug"]),
]
def __str__(self): def __str__(self):
return self.name return self.name
@ -228,6 +232,10 @@ class ManagedDocument(models.Model):
ordering = ["-updated_at", "title"] ordering = ["-updated_at", "title"]
verbose_name = _("Managed document") verbose_name = _("Managed document")
verbose_name_plural = _("Managed documents") verbose_name_plural = _("Managed documents")
indexes = [
models.Index(fields=["is_archived", "-updated_at"]),
models.Index(fields=["title"]),
]
def __str__(self): def __str__(self):
return self.title return self.title
@ -590,6 +598,9 @@ class DocumentAttachment(models.Model):
unique_together = ("document", "content_type", "object_id") unique_together = ("document", "content_type", "object_id")
verbose_name = _("Attachment") verbose_name = _("Attachment")
verbose_name_plural = _("Attachments") verbose_name_plural = _("Attachments")
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
def __str__(self): def __str__(self):
return f"{self.document} -> {self.content_object}" return f"{self.document} -> {self.content_object}"

View file

@ -87,49 +87,15 @@ def get_folder_navigation_context(user, current_folder=None):
breadcrumbs = get_folder_breadcrumb(current_folder) breadcrumbs = get_folder_breadcrumb(current_folder)
return { return {
"root_folders": get_root_folders(user), "root_folders": get_root_folders(user),
"folder_tree": build_folder_tree(user), "folder_tree": build_folder_tree(user, current_folder=current_folder),
"active_folder_slug": current_folder.slug if current_folder else None, "active_folder_slug": current_folder.slug if current_folder else None,
"active_trail_slugs": [folder.slug for folder in breadcrumbs], "active_trail_slugs": [folder.slug for folder in breadcrumbs],
"breadcrumbs": breadcrumbs, "breadcrumbs": breadcrumbs,
} }
def build_folder_tree(user): def build_folder_tree(user, current_folder=None):
visible_ids = get_user_visible_folder_ids(user) return []
if not visible_ids:
return []
folders = (
DocumentFolder.objects.prefetch_related("parent_folders")
.filter(pk__in=visible_ids)
.order_by("name")
.distinct()
)
children_map = defaultdict(list)
root_nodes = []
for folder in folders:
parents = list(folder.parent_folders.all())
if parents:
for parent in parents:
children_map[parent.pk].append(folder)
else:
root_nodes.append(folder)
def sort_children(children):
return sorted(children, key=lambda child: child.name.lower())
def build_nodes(nodes):
tree = []
for node in sort_children(nodes):
tree.append(
{
"folder": node,
"children": build_nodes(children_map[node.pk]),
}
)
return tree
return build_nodes(root_nodes)
def get_descendant_folder_ids(folder, visible_ids=None): def get_descendant_folder_ids(folder, visible_ids=None):
@ -287,10 +253,10 @@ class DocumentListView(DocumentsAppAccessMixin, LoginRequiredMixin, ListView):
raise Http404 raise Http404
if self.current_folder: if self.current_folder:
self.descendant_folder_ids = get_descendant_folder_ids(
self.current_folder, visible_ids=self.visible_folder_ids
)
if self.search_term: if self.search_term:
self.descendant_folder_ids = get_descendant_folder_ids(
self.current_folder, visible_ids=self.visible_folder_ids
)
queryset = queryset.filter( queryset = queryset.filter(
folders__pk__in=self.descendant_folder_ids folders__pk__in=self.descendant_folder_ids
) )
@ -965,13 +931,13 @@ class DocumentDetailView(DocumentsAppAccessMixin, LoginRequiredMixin, DetailView
for version in document.versions.all() for version in document.versions.all()
for validation in version.validations.all() for validation in version.validations.all()
} }
context["root_folders"] = get_root_folders(self.request.user)
context["folder_tree"] = build_folder_tree(self.request.user)
requested_folder_slug = self.request.GET.get("folder") requested_folder_slug = self.request.GET.get("folder")
navigation_folder = get_navigation_folder_for_document( navigation_folder = get_navigation_folder_for_document(
document, requested_slug=requested_folder_slug document, requested_slug=requested_folder_slug
) )
context.update(
get_folder_navigation_context(self.request.user, current_folder=navigation_folder)
)
# Check if the document has a current version file ending in .dxf and is attached to a TrafficLightIntersection # Check if the document has a current version file ending in .dxf and is attached to a TrafficLightIntersection
is_dxf = False is_dxf = False