diff --git a/loko/common/templatetags/short_name.py b/loko/common/templatetags/short_name.py index 28215f0..859ad62 100644 --- a/loko/common/templatetags/short_name.py +++ b/loko/common/templatetags/short_name.py @@ -1,20 +1,16 @@ from django import template import os +from common.utils import get_short_name, get_user_company, get_user_display_name register = template.Library() + @register.filter def short_name(user): """ Retourne 'Prénom N.' à partir d'un objet User. """ - if not user: - return "" - first = getattr(user, "first_name", "") or "" - last = getattr(user, "last_name", "") or "" - initial = (last[0].upper() + ".") if last else "" - res = f"{first} {initial}".strip() - return res if res else getattr(user, "username", "") + return get_short_name(user) @register.filter @@ -22,20 +18,16 @@ def user_company(user): """ Retourne le nom de la société de l'utilisateur. """ - if not user: - return "" - if hasattr(user, 'config') and user.config and user.config.company: - return user.config.company.name - try: - from contracts.models import CompanyMember - member = CompanyMember.objects.filter(user=user).prefetch_related('teams__company').first() - if member: - for team in member.teams.all(): - if team.company: - return team.company.name - except Exception: - pass - return "" + return get_user_company(user) + + +@register.filter +def user_display(user): + """ + Retourne 'Prénom N. (Organisme)' ou 'Prénom N.' si aucun organisme n'est associé. + """ + return get_user_display_name(user) + @register.filter diff --git a/loko/common/utils.py b/loko/common/utils.py index 344f6f8..a5ebfdd 100644 --- a/loko/common/utils.py +++ b/loko/common/utils.py @@ -126,6 +126,42 @@ def get_short_name(user): return res if res else getattr(user, "username", "") +def get_user_company(user): + """ + Retourne le nom de l'organisme / société de l'utilisateur. + """ + if not user: + return "" + if hasattr(user, "config") and user.config and user.config.company: + return user.config.company.name + try: + from contracts.models import CompanyMember + member = CompanyMember.objects.filter(user=user).prefetch_related("teams__company").first() + if member: + for team in member.teams.all(): + if team.company: + return team.company.name + except Exception: + pass + return "" + + +def get_user_display_name(user, include_company=True): + """ + Retourne 'Prénom N. (Organisme)' ou 'Prénom N.' + """ + if not user: + return "" + name = get_short_name(user) + if not include_company: + return name + company = get_user_company(user) + if company: + return f"{name} ({company})" + return name + + + from django.utils.translation import gettext_lazy as _ REGIONAL_TERMINOLOGY = { diff --git a/loko/documents/forms.py b/loko/documents/forms.py index d2bd38d..ccb1615 100644 --- a/loko/documents/forms.py +++ b/loko/documents/forms.py @@ -23,7 +23,7 @@ from .permissions import ( user_can_change_visibility, ) from common.models import Thematic -from common.utils import get_short_name +from common.utils import get_short_name, get_user_display_name User = get_user_model() @@ -66,11 +66,15 @@ class DocumentUpdateForm(forms.ModelForm): self.fields["folders"].widget.attrs.setdefault("class", "form-select") self.fields["folders"].required = False if self.user: - self.fields["folders"].queryset = filter_folders_for_user( - DocumentFolder.objects.all(), + accessible_folders = filter_folders_for_user( + DocumentFolder.objects.prefetch_related("parent_folders"), self.user, required_permission=ManagedDocument.PERMISSION_VIEW, - ).order_by("name") + ) + 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() def clean_visibility(self): if self.user and not user_can_change_visibility(self.user): @@ -108,11 +112,14 @@ class ManagedDocumentForm(forms.ModelForm): # Limiter les folders accessibles à l'utilisateur if user: accessible_folders = filter_folders_for_user( - DocumentFolder.objects.all(), + DocumentFolder.objects.prefetch_related("parent_folders"), user, required_permission=ManagedDocument.PERMISSION_VIEW ) - self.fields["folders"].queryset = accessible_folders.order_by("name") + 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() # Pré-sélectionner le folder initial si fourni if initial_folder and not self.instance.pk: @@ -174,60 +181,136 @@ class DocumentVersionForm(forms.ModelForm): self.fields["comment"].widget.attrs.setdefault("class", "form-control") -class DocumentShareForm(forms.ModelForm): - class Meta: - model = DocumentShare - fields = ["user", "permission"] - widgets = { - "permission": forms.Select(attrs={"class": "form-select"}), - } +class DocumentShareForm(forms.Form): + users = forms.ModelMultipleChoiceField( + queryset=User.objects.none(), + label=_("Users"), + widget=forms.SelectMultiple( + attrs={ + "class": "form-select select2-users", + "id": "id_share_users", + "style": "width: 100%;", + } + ), + ) + permission = forms.ChoiceField( + choices=ManagedDocument.PERMISSION_CHOICES, + initial=ManagedDocument.PERMISSION_VIEW, + label=_("Permission"), + widget=forms.Select(attrs={"class": "form-select"}), + ) def __init__(self, *args, **kwargs): document = kwargs.pop("document", None) + if args and isinstance(args[0], dict): + data = args[0].copy() + if "user" in data and "users" not in data: + val = data.getlist("user") if hasattr(data, "getlist") else [data["user"]] + if hasattr(data, "setlist"): + data.setlist("users", val) + else: + data["users"] = val + args = (data,) + args[1:] + elif kwargs.get("data"): + data = kwargs["data"].copy() + if "user" in data and "users" not in data: + val = data.getlist("user") if hasattr(data, "getlist") else [data["user"]] + if hasattr(data, "setlist"): + data.setlist("users", val) + else: + data["users"] = val + kwargs["data"] = data + super().__init__(*args, **kwargs) + qs = User.objects.filter(is_active=True).select_related("config__company") if document: - self.fields["user"].queryset = User.objects.exclude(document_shares__document=document) - self.fields["user"].widget.attrs.setdefault("class", "form-select") - self.fields["user"].label_from_instance = lambda obj: get_short_name(obj) or obj.username + qs = qs.exclude(document_shares__document=document) + user_list = sorted(qs, key=lambda u: get_user_display_name(u).lower()) + self.fields["users"].queryset = qs + self.fields["users"].choices = [(u.pk, get_user_display_name(u)) for u in user_list] + self.fields["users"].label_from_instance = lambda obj: get_user_display_name(obj) - def save(self, commit=True, creator=None, document=None): - share = super().save(commit=False) - if document: - share.document = document - if creator and not share.created_by: - share.created_by = creator - if commit: - share.save() - return share + def save(self, creator=None, document=None): + users = self.cleaned_data.get("users", []) + permission = self.cleaned_data.get("permission") + shares = [] + for user in users: + share, _ = DocumentShare.objects.update_or_create( + document=document, + user=user, + defaults={ + "permission": permission, + "created_by": creator, + }, + ) + shares.append(share) + return shares -class DocumentFolderShareForm(forms.ModelForm): - class Meta: - model = DocumentFolderShare - fields = ["user", "permission"] - widgets = { - "permission": forms.Select(attrs={"class": "form-select"}), - } +class DocumentFolderShareForm(forms.Form): + users = forms.ModelMultipleChoiceField( + queryset=User.objects.none(), + label=_("Users"), + widget=forms.SelectMultiple( + attrs={ + "class": "form-select select2-users", + "id": "id_folder_share_users", + "style": "width: 100%;", + } + ), + ) + permission = forms.ChoiceField( + choices=ManagedDocument.PERMISSION_CHOICES, + initial=ManagedDocument.PERMISSION_VIEW, + label=_("Permission"), + widget=forms.Select(attrs={"class": "form-select"}), + ) def __init__(self, *args, **kwargs): folder = kwargs.pop("folder", None) + if args and isinstance(args[0], dict): + data = args[0].copy() + if "user" in data and "users" not in data: + val = data.getlist("user") if hasattr(data, "getlist") else [data["user"]] + if hasattr(data, "setlist"): + data.setlist("users", val) + else: + data["users"] = val + args = (data,) + args[1:] + elif kwargs.get("data"): + data = kwargs["data"].copy() + if "user" in data and "users" not in data: + val = data.getlist("user") if hasattr(data, "getlist") else [data["user"]] + if hasattr(data, "setlist"): + data.setlist("users", val) + else: + data["users"] = val + kwargs["data"] = data + super().__init__(*args, **kwargs) + qs = User.objects.filter(is_active=True).select_related("config__company") if folder: - self.fields["user"].queryset = User.objects.exclude( - document_folder_shares__folder=folder + qs = qs.exclude(document_folder_shares__folder=folder) + user_list = sorted(qs, key=lambda u: get_user_display_name(u).lower()) + self.fields["users"].queryset = qs + self.fields["users"].choices = [(u.pk, get_user_display_name(u)) for u in user_list] + self.fields["users"].label_from_instance = lambda obj: get_user_display_name(obj) + + def save(self, creator=None, folder=None): + users = self.cleaned_data.get("users", []) + permission = self.cleaned_data.get("permission") + shares = [] + for user in users: + share, _ = DocumentFolderShare.objects.update_or_create( + folder=folder, + user=user, + defaults={ + "permission": permission, + "created_by": creator, + }, ) - self.fields["user"].widget.attrs.setdefault("class", "form-select") - self.fields["user"].label_from_instance = lambda obj: get_short_name(obj) or obj.username - - def save(self, commit=True, creator=None, folder=None): - share = super().save(commit=False) - if folder: - share.folder = folder - if creator and not share.created_by: - share.created_by = creator - if commit: - share.save() - return share + shares.append(share) + return shares class DocumentValidationForm(forms.ModelForm): @@ -397,11 +480,15 @@ class DocumentFolderForm(forms.ModelForm): instance = getattr(self, "instance", None) if instance and instance.pk: excluded_ids = {instance.pk, *self._get_descendant_ids(instance)} - self.fields["parent_folders"].queryset = ( - DocumentFolder.objects.exclude(pk__in=excluded_ids).order_by("name") + parent_qs = ( + DocumentFolder.objects.prefetch_related("parent_folders").exclude(pk__in=excluded_ids) ) else: - self.fields["parent_folders"].queryset = DocumentFolder.objects.order_by("name") + parent_qs = DocumentFolder.objects.prefetch_related("parent_folders") + sorted_parents = sorted(parent_qs, key=lambda f: f.get_full_path().lower()) + 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: self.fields["thematics"].queryset = Thematic.objects.order_by("name_fr") @@ -415,8 +502,11 @@ class DocumentFolderForm(forms.ModelForm): if "created_by" in self.fields: self.fields["created_by"].label = _("Owner") self.fields["created_by"].required = False - self.fields["created_by"].queryset = User.objects.order_by("username") - self.fields["created_by"].label_from_instance = lambda obj: get_short_name(obj) or obj.username + user_qs = User.objects.filter(is_active=True).select_related("config__company") + user_list = sorted(user_qs, key=lambda u: get_user_display_name(u).lower()) + self.fields["created_by"].queryset = user_qs + self.fields["created_by"].choices = [(u.pk, get_user_display_name(u)) for u in user_list] + self.fields["created_by"].label_from_instance = lambda obj: get_user_display_name(obj) or obj.username if not self._can_edit_owner(): self.fields.pop("created_by", None) diff --git a/loko/documents/models.py b/loko/documents/models.py index 095f886..df87fb0 100644 --- a/loko/documents/models.py +++ b/loko/documents/models.py @@ -167,8 +167,37 @@ class DocumentFolder(models.Model): models.Index(fields=["slug"]), ] + def get_full_path(self, separator=" / "): + """ + Retourne le chemin complet du dossier depuis la racine (ex: 'Anderlecht / 25-XPERT / As-Built'). + """ + if not self.name: + return "" + parts = [self.name] + visited = {self.pk} if self.pk else set() + current = self + while current and current.pk: + try: + parents = list(current.parent_folders.all()) + except Exception: + break + if not parents: + break + parents.sort(key=lambda p: (p.name or "").lower()) + parent = parents[0] + if not parent.pk or parent.pk in visited: + break + visited.add(parent.pk) + parts.append(parent.name) + current = parent + return separator.join(reversed(parts)) + + @property + def full_path(self): + return self.get_full_path() + def __str__(self): - return self.name + return self.get_full_path() def save(self, *args, **kwargs): if not self.slug: diff --git a/loko/documents/templates/documents/document_detail.html b/loko/documents/templates/documents/document_detail.html index 7015bd2..9d792b2 100644 --- a/loko/documents/templates/documents/document_detail.html +++ b/loko/documents/templates/documents/document_detail.html @@ -130,7 +130,7 @@ @@ -440,12 +440,14 @@