feat: add bulk user selection for sharing, implement hierarchical path display, and standardize user naming conventions.
This commit is contained in:
parent
a098a66204
commit
15b058e246
8 changed files with 362 additions and 88 deletions
|
|
@ -1,20 +1,16 @@
|
||||||
from django import template
|
from django import template
|
||||||
import os
|
import os
|
||||||
|
from common.utils import get_short_name, get_user_company, get_user_display_name
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def short_name(user):
|
def short_name(user):
|
||||||
"""
|
"""
|
||||||
Retourne 'Prénom N.' à partir d'un objet User.
|
Retourne 'Prénom N.' à partir d'un objet User.
|
||||||
"""
|
"""
|
||||||
if not user:
|
return get_short_name(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", "")
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
|
|
@ -22,20 +18,16 @@ def user_company(user):
|
||||||
"""
|
"""
|
||||||
Retourne le nom de la société de l'utilisateur.
|
Retourne le nom de la société de l'utilisateur.
|
||||||
"""
|
"""
|
||||||
if not user:
|
return get_user_company(user)
|
||||||
return ""
|
|
||||||
if hasattr(user, 'config') and user.config and user.config.company:
|
|
||||||
return user.config.company.name
|
@register.filter
|
||||||
try:
|
def user_display(user):
|
||||||
from contracts.models import CompanyMember
|
"""
|
||||||
member = CompanyMember.objects.filter(user=user).prefetch_related('teams__company').first()
|
Retourne 'Prénom N. (Organisme)' ou 'Prénom N.' si aucun organisme n'est associé.
|
||||||
if member:
|
"""
|
||||||
for team in member.teams.all():
|
return get_user_display_name(user)
|
||||||
if team.company:
|
|
||||||
return team.company.name
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,42 @@ def get_short_name(user):
|
||||||
return res if res else getattr(user, "username", "")
|
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 _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
REGIONAL_TERMINOLOGY = {
|
REGIONAL_TERMINOLOGY = {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from .permissions import (
|
||||||
user_can_change_visibility,
|
user_can_change_visibility,
|
||||||
)
|
)
|
||||||
from common.models import Thematic
|
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()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
@ -66,11 +66,15 @@ class DocumentUpdateForm(forms.ModelForm):
|
||||||
self.fields["folders"].widget.attrs.setdefault("class", "form-select")
|
self.fields["folders"].widget.attrs.setdefault("class", "form-select")
|
||||||
self.fields["folders"].required = False
|
self.fields["folders"].required = False
|
||||||
if self.user:
|
if self.user:
|
||||||
self.fields["folders"].queryset = filter_folders_for_user(
|
accessible_folders = filter_folders_for_user(
|
||||||
DocumentFolder.objects.all(),
|
DocumentFolder.objects.prefetch_related("parent_folders"),
|
||||||
self.user,
|
self.user,
|
||||||
required_permission=ManagedDocument.PERMISSION_VIEW,
|
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):
|
def clean_visibility(self):
|
||||||
if self.user and not user_can_change_visibility(self.user):
|
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
|
# Limiter les folders accessibles à l'utilisateur
|
||||||
if user:
|
if user:
|
||||||
accessible_folders = filter_folders_for_user(
|
accessible_folders = filter_folders_for_user(
|
||||||
DocumentFolder.objects.all(),
|
DocumentFolder.objects.prefetch_related("parent_folders"),
|
||||||
user,
|
user,
|
||||||
required_permission=ManagedDocument.PERMISSION_VIEW
|
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
|
# Pré-sélectionner le folder initial si fourni
|
||||||
if initial_folder and not self.instance.pk:
|
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")
|
self.fields["comment"].widget.attrs.setdefault("class", "form-control")
|
||||||
|
|
||||||
|
|
||||||
class DocumentShareForm(forms.ModelForm):
|
class DocumentShareForm(forms.Form):
|
||||||
class Meta:
|
users = forms.ModelMultipleChoiceField(
|
||||||
model = DocumentShare
|
queryset=User.objects.none(),
|
||||||
fields = ["user", "permission"]
|
label=_("Users"),
|
||||||
widgets = {
|
widget=forms.SelectMultiple(
|
||||||
"permission": forms.Select(attrs={"class": "form-select"}),
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
document = kwargs.pop("document", None)
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
|
qs = User.objects.filter(is_active=True).select_related("config__company")
|
||||||
if document:
|
if document:
|
||||||
self.fields["user"].queryset = User.objects.exclude(document_shares__document=document)
|
qs = qs.exclude(document_shares__document=document)
|
||||||
self.fields["user"].widget.attrs.setdefault("class", "form-select")
|
user_list = sorted(qs, key=lambda u: get_user_display_name(u).lower())
|
||||||
self.fields["user"].label_from_instance = lambda obj: get_short_name(obj) or obj.username
|
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):
|
def save(self, creator=None, document=None):
|
||||||
share = super().save(commit=False)
|
users = self.cleaned_data.get("users", [])
|
||||||
if document:
|
permission = self.cleaned_data.get("permission")
|
||||||
share.document = document
|
shares = []
|
||||||
if creator and not share.created_by:
|
for user in users:
|
||||||
share.created_by = creator
|
share, _ = DocumentShare.objects.update_or_create(
|
||||||
if commit:
|
document=document,
|
||||||
share.save()
|
user=user,
|
||||||
return share
|
defaults={
|
||||||
|
"permission": permission,
|
||||||
|
"created_by": creator,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
shares.append(share)
|
||||||
|
return shares
|
||||||
|
|
||||||
|
|
||||||
class DocumentFolderShareForm(forms.ModelForm):
|
class DocumentFolderShareForm(forms.Form):
|
||||||
class Meta:
|
users = forms.ModelMultipleChoiceField(
|
||||||
model = DocumentFolderShare
|
queryset=User.objects.none(),
|
||||||
fields = ["user", "permission"]
|
label=_("Users"),
|
||||||
widgets = {
|
widget=forms.SelectMultiple(
|
||||||
"permission": forms.Select(attrs={"class": "form-select"}),
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
folder = kwargs.pop("folder", None)
|
folder = kwargs.pop("folder", None)
|
||||||
super().__init__(*args, **kwargs)
|
if args and isinstance(args[0], dict):
|
||||||
if folder:
|
data = args[0].copy()
|
||||||
self.fields["user"].queryset = User.objects.exclude(
|
if "user" in data and "users" not in data:
|
||||||
document_folder_shares__folder=folder
|
val = data.getlist("user") if hasattr(data, "getlist") else [data["user"]]
|
||||||
)
|
if hasattr(data, "setlist"):
|
||||||
self.fields["user"].widget.attrs.setdefault("class", "form-select")
|
data.setlist("users", val)
|
||||||
self.fields["user"].label_from_instance = lambda obj: get_short_name(obj) or obj.username
|
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
|
||||||
|
|
||||||
def save(self, commit=True, creator=None, folder=None):
|
super().__init__(*args, **kwargs)
|
||||||
share = super().save(commit=False)
|
qs = User.objects.filter(is_active=True).select_related("config__company")
|
||||||
if folder:
|
if folder:
|
||||||
share.folder = folder
|
qs = qs.exclude(document_folder_shares__folder=folder)
|
||||||
if creator and not share.created_by:
|
user_list = sorted(qs, key=lambda u: get_user_display_name(u).lower())
|
||||||
share.created_by = creator
|
self.fields["users"].queryset = qs
|
||||||
if commit:
|
self.fields["users"].choices = [(u.pk, get_user_display_name(u)) for u in user_list]
|
||||||
share.save()
|
self.fields["users"].label_from_instance = lambda obj: get_user_display_name(obj)
|
||||||
return share
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
shares.append(share)
|
||||||
|
return shares
|
||||||
|
|
||||||
|
|
||||||
class DocumentValidationForm(forms.ModelForm):
|
class DocumentValidationForm(forms.ModelForm):
|
||||||
|
|
@ -397,11 +480,15 @@ class DocumentFolderForm(forms.ModelForm):
|
||||||
instance = getattr(self, "instance", None)
|
instance = getattr(self, "instance", None)
|
||||||
if instance and instance.pk:
|
if instance and instance.pk:
|
||||||
excluded_ids = {instance.pk, *self._get_descendant_ids(instance)}
|
excluded_ids = {instance.pk, *self._get_descendant_ids(instance)}
|
||||||
self.fields["parent_folders"].queryset = (
|
parent_qs = (
|
||||||
DocumentFolder.objects.exclude(pk__in=excluded_ids).order_by("name")
|
DocumentFolder.objects.prefetch_related("parent_folders").exclude(pk__in=excluded_ids)
|
||||||
)
|
)
|
||||||
else:
|
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:
|
if "thematics" in self.fields:
|
||||||
self.fields["thematics"].queryset = Thematic.objects.order_by("name_fr")
|
self.fields["thematics"].queryset = Thematic.objects.order_by("name_fr")
|
||||||
|
|
@ -415,8 +502,11 @@ class DocumentFolderForm(forms.ModelForm):
|
||||||
if "created_by" in self.fields:
|
if "created_by" in self.fields:
|
||||||
self.fields["created_by"].label = _("Owner")
|
self.fields["created_by"].label = _("Owner")
|
||||||
self.fields["created_by"].required = False
|
self.fields["created_by"].required = False
|
||||||
self.fields["created_by"].queryset = User.objects.order_by("username")
|
user_qs = User.objects.filter(is_active=True).select_related("config__company")
|
||||||
self.fields["created_by"].label_from_instance = lambda obj: get_short_name(obj) or obj.username
|
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():
|
if not self._can_edit_owner():
|
||||||
self.fields.pop("created_by", None)
|
self.fields.pop("created_by", None)
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,37 @@ class DocumentFolder(models.Model):
|
||||||
models.Index(fields=["slug"]),
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.get_full_path()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
<select name="folder_id" id="id_add_folder_select" class="form-select" required>
|
<select name="folder_id" id="id_add_folder_select" class="form-select" required>
|
||||||
<option value="">-- {% translate "Select a folder" %} --</option>
|
<option value="">-- {% translate "Select a folder" %} --</option>
|
||||||
{% for f in available_folders %}
|
{% for f in available_folders %}
|
||||||
<option value="{{ f.pk }}">{{ f.name }}</option>
|
<option value="{{ f.pk }}">{{ f.get_full_path }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="btn btn-primary">{% translate "Add" %}</button>
|
<button type="submit" class="btn btn-primary">{% translate "Add" %}</button>
|
||||||
|
|
@ -440,12 +440,14 @@
|
||||||
<div class="alert alert-danger">{{ share_form.non_field_errors }}</div>
|
<div class="alert alert-danger">{{ share_form.non_field_errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
{{ share_form.user }}
|
<label class="form-label small fw-semibold" for="{{ share_form.users.id_for_label }}">{% translate "Users" %}</label>
|
||||||
{% if share_form.user.errors %}
|
{{ share_form.users }}
|
||||||
<div class="text-danger small">{{ share_form.user.errors }}</div>
|
{% if share_form.users.errors %}
|
||||||
|
<div class="text-danger small">{{ share_form.users.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
|
<label class="form-label small fw-semibold" for="{{ share_form.permission.id_for_label }}">{% translate "Permission" %}</label>
|
||||||
{{ share_form.permission }}
|
{{ share_form.permission }}
|
||||||
{% if share_form.permission.errors %}
|
{% if share_form.permission.errors %}
|
||||||
<div class="text-danger small">{{ share_form.permission.errors }}</div>
|
<div class="text-danger small">{{ share_form.permission.errors }}</div>
|
||||||
|
|
@ -456,7 +458,7 @@
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for share in document.shares.all %}
|
{% for share in document.shares.all %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span>{{ share.user|short_name }} – {{ share.get_permission_display }}</span>
|
<span>{{ share.user|user_display }} – {{ share.get_permission_display }}</span>
|
||||||
<form method="post" action="{% url 'documents:delete_share' document.pk share.pk %}">
|
<form method="post" action="{% url 'documents:delete_share' document.pk share.pk %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn btn-sm btn-outline-danger" type="submit"><i class="bi bi-x"></i></button>
|
<button class="btn btn-sm btn-outline-danger" type="submit"><i class="bi bi-x"></i></button>
|
||||||
|
|
@ -759,7 +761,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
<label for="id_doc_folders" class="form-label fw-bold">{% translate "Répertoires" %}</label>
|
<label for="id_doc_folders" class="form-label fw-bold">{% translate "Répertoires" %}</label>
|
||||||
<select name="folders" id="id_doc_folders" class="form-select" multiple>
|
<select name="folders" id="id_doc_folders" class="form-select" multiple>
|
||||||
{% for f in all_accessible_folders %}
|
{% for f in all_accessible_folders %}
|
||||||
<option value="{{ f.pk }}" {% if f in document.folders.all %}selected{% endif %}>{{ f.name }}</option>
|
<option value="{{ f.pk }}" {% if f in document.folders.all %}selected{% endif %}>{{ f.get_full_path }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -811,6 +813,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select2 for document share users
|
||||||
|
if ($('#id_share_users').length) {
|
||||||
|
$('#id_share_users').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
placeholder: "{% translate 'Rechercher et sélectionner des utilisateurs...' %}",
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const $contentType = $('#id_content_type');
|
const $contentType = $('#id_content_type');
|
||||||
const $objectId = $('#id_object_id');
|
const $objectId = $('#id_object_id');
|
||||||
const $searchInput = $('#object_search_input');
|
const $searchInput = $('#object_search_input');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
{% extends "documents/base_documents.html" %}
|
{% extends "documents/base_documents.html" %}
|
||||||
{% load i18n short_name %}
|
{% load i18n static short_name %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{% static 'common/select2.min.css' %}" />
|
||||||
|
<link rel="stylesheet" href="{% static 'common/select2-bootstrap-5-theme.min.css' %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
|
|
@ -42,10 +47,10 @@
|
||||||
<form method="post" action="{% url 'documents:folder_share_add' folder.pk %}">
|
<form method="post" action="{% url 'documents:folder_share_add' folder.pk %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="{{ share_form.user.id_for_label }}">{% translate "User" %}</label>
|
<label class="form-label" for="{{ share_form.users.id_for_label }}">{% translate "Users" %}</label>
|
||||||
{{ share_form.user }}
|
{{ share_form.users }}
|
||||||
{% if share_form.user.errors %}
|
{% if share_form.users.errors %}
|
||||||
<div class="invalid-feedback d-block">{{ share_form.user.errors|join:', ' }}</div>
|
<div class="invalid-feedback d-block">{{ share_form.users.errors|join:', ' }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|
@ -63,7 +68,7 @@
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for share in shares %}
|
{% for share in shares %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span>{{ share.user|short_name }} – {{ share.get_permission_display }}</span>
|
<span>{{ share.user|user_display }} – {{ share.get_permission_display }}</span>
|
||||||
<form method="post" action="{% url 'documents:folder_share_delete' folder.pk share.pk %}" class="ms-2">
|
<form method="post" action="{% url 'documents:folder_share_delete' folder.pk share.pk %}" class="ms-2">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
|
@ -145,3 +150,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'common/select2.min.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
if ($('#id_folder_share_users').length) {
|
||||||
|
$('#id_folder_share_users').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
placeholder: "{% translate 'Rechercher et sélectionner des utilisateurs...' %}",
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1074,4 +1074,91 @@ class DocumentVisibilityAndIndependenceTests(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(link_response.status_code, 403)
|
self.assertEqual(link_response.status_code, 403)
|
||||||
|
|
||||||
|
def test_folder_full_path_hierarchy(self):
|
||||||
|
from .forms import ManagedDocumentForm, DocumentFolderForm
|
||||||
|
|
||||||
|
commune = DocumentFolder.objects.create(name="Anderlecht", slug="anderlecht", created_by=self.creator)
|
||||||
|
carrefour = DocumentFolder.objects.create(name="Carrefour 1", slug="carrefour-1", created_by=self.creator)
|
||||||
|
carrefour.parent_folders.add(commune)
|
||||||
|
asbuilt = DocumentFolder.objects.create(name="As-Built", slug="asbuilt-1", created_by=self.creator)
|
||||||
|
asbuilt.parent_folders.add(carrefour)
|
||||||
|
|
||||||
|
# Test get_full_path on models
|
||||||
|
self.assertEqual(commune.get_full_path(), "Anderlecht")
|
||||||
|
self.assertEqual(carrefour.get_full_path(), "Anderlecht / Carrefour 1")
|
||||||
|
self.assertEqual(asbuilt.get_full_path(), "Anderlecht / Carrefour 1 / As-Built")
|
||||||
|
self.assertEqual(str(asbuilt), "Anderlecht / Carrefour 1 / As-Built")
|
||||||
|
|
||||||
|
# Test ManagedDocumentForm choices
|
||||||
|
form = ManagedDocumentForm(user=self.creator)
|
||||||
|
folder_choices = dict(form.fields["folders"].choices)
|
||||||
|
self.assertEqual(folder_choices[asbuilt.pk], "Anderlecht / Carrefour 1 / As-Built")
|
||||||
|
|
||||||
|
# Test DocumentFolderForm choices
|
||||||
|
folder_form = DocumentFolderForm(user=self.creator)
|
||||||
|
parent_choices = dict(folder_form.fields["parent_folders"].choices)
|
||||||
|
self.assertEqual(parent_choices[carrefour.pk], "Anderlecht / Carrefour 1")
|
||||||
|
|
||||||
|
def test_user_display_name_with_company(self):
|
||||||
|
from common.utils import get_user_display_name
|
||||||
|
from common.models import UserConfig
|
||||||
|
from contracts.models import Company
|
||||||
|
from .models import VisibilityScope
|
||||||
|
|
||||||
|
user_with_comp = User.objects.create_user(
|
||||||
|
username="jdupont", first_name="Jean", last_name="Dupont", password="pass"
|
||||||
|
)
|
||||||
|
company = Company.objects.create(name="Bruxelles Mobilité")
|
||||||
|
UserConfig.objects.create(user=user_with_comp, company=company)
|
||||||
|
|
||||||
|
user_without_comp = User.objects.create_user(
|
||||||
|
username="msmith", first_name="Mary", last_name="Smith", password="pass"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(get_user_display_name(user_with_comp), "Jean D. (Bruxelles Mobilité)")
|
||||||
|
self.assertEqual(get_user_display_name(user_without_comp), "Mary S.")
|
||||||
|
|
||||||
|
def test_multiple_users_share_folder_and_document(self):
|
||||||
|
from .forms import DocumentShareForm, DocumentFolderShareForm
|
||||||
|
from .models import VisibilityScope
|
||||||
|
|
||||||
|
u1 = User.objects.create_user(username="target1", first_name="Alice", last_name="A", password="pass")
|
||||||
|
u2 = User.objects.create_user(username="target2", first_name="Bob", last_name="B", password="pass")
|
||||||
|
|
||||||
|
# Test folder sharing with multiple users
|
||||||
|
folder = DocumentFolder.objects.create(name="Shared Folder", created_by=self.creator)
|
||||||
|
self.client.force_login(self.creator)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("documents:folder_share_add", args=[folder.pk]),
|
||||||
|
{
|
||||||
|
"users": [u1.pk, u2.pk],
|
||||||
|
"permission": ManagedDocument.PERMISSION_VIEW,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(folder.shares.count(), 2)
|
||||||
|
self.assertTrue(folder.shares.filter(user=u1).exists())
|
||||||
|
self.assertTrue(folder.shares.filter(user=u2).exists())
|
||||||
|
|
||||||
|
# Test document sharing with multiple users
|
||||||
|
doc = ManagedDocument.objects.create(
|
||||||
|
title="Shared Doc",
|
||||||
|
created_by=self.creator,
|
||||||
|
visibility=VisibilityScope.INTERNAL,
|
||||||
|
)
|
||||||
|
doc_share_response = self.client.post(
|
||||||
|
reverse("documents:share", args=[doc.pk]),
|
||||||
|
{
|
||||||
|
"users": [u1.pk, u2.pk],
|
||||||
|
"permission": ManagedDocument.PERMISSION_EDIT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(doc_share_response.status_code, 302)
|
||||||
|
self.assertEqual(doc.shares.count(), 2)
|
||||||
|
self.assertEqual(doc.shares.get(user=u1).permission, ManagedDocument.PERMISSION_EDIT)
|
||||||
|
self.assertEqual(doc.shares.get(user=u2).permission, ManagedDocument.PERMISSION_EDIT)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1098,12 +1098,18 @@ class DocumentDetailView(DocumentsAppAccessMixin, LoginRequiredMixin, DetailView
|
||||||
# Available folders/tags for add forms (exclude already associated ones)
|
# Available folders/tags for add forms (exclude already associated ones)
|
||||||
existing_folder_ids = list(document.folders.values_list("pk", flat=True))
|
existing_folder_ids = list(document.folders.values_list("pk", flat=True))
|
||||||
accessible_folders = filter_folders_for_user(
|
accessible_folders = filter_folders_for_user(
|
||||||
DocumentFolder.objects.all(),
|
DocumentFolder.objects.prefetch_related("parent_folders"),
|
||||||
self.request.user,
|
self.request.user,
|
||||||
required_permission=ManagedDocument.PERMISSION_VIEW,
|
required_permission=ManagedDocument.PERMISSION_VIEW,
|
||||||
)
|
)
|
||||||
context["available_folders"] = accessible_folders.exclude(pk__in=existing_folder_ids).order_by("name")
|
context["available_folders"] = sorted(
|
||||||
context["all_accessible_folders"] = accessible_folders.order_by("name")
|
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))
|
existing_tag_ids = list(document.tags.values_list("pk", flat=True))
|
||||||
context["available_tags"] = DocumentTag.objects.exclude(
|
context["available_tags"] = DocumentTag.objects.exclude(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue