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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@
|
|||
<select name="folder_id" id="id_add_folder_select" class="form-select" required>
|
||||
<option value="">-- {% translate "Select a folder" %} --</option>
|
||||
{% for f in available_folders %}
|
||||
<option value="{{ f.pk }}">{{ f.name }}</option>
|
||||
<option value="{{ f.pk }}">{{ f.get_full_path }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<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>
|
||||
{% endif %}
|
||||
<div class="mb-2">
|
||||
{{ share_form.user }}
|
||||
{% if share_form.user.errors %}
|
||||
<div class="text-danger small">{{ share_form.user.errors }}</div>
|
||||
<label class="form-label small fw-semibold" for="{{ share_form.users.id_for_label }}">{% translate "Users" %}</label>
|
||||
{{ share_form.users }}
|
||||
{% if share_form.users.errors %}
|
||||
<div class="text-danger small">{{ share_form.users.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold" for="{{ share_form.permission.id_for_label }}">{% translate "Permission" %}</label>
|
||||
{{ share_form.permission }}
|
||||
{% if share_form.permission.errors %}
|
||||
<div class="text-danger small">{{ share_form.permission.errors }}</div>
|
||||
|
|
@ -456,7 +458,7 @@
|
|||
<ul class="list-group list-group-flush">
|
||||
{% for share in document.shares.all %}
|
||||
<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 %}">
|
||||
{% csrf_token %}
|
||||
<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>
|
||||
<select name="folders" id="id_doc_folders" class="form-select" multiple>
|
||||
{% 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 %}
|
||||
</select>
|
||||
</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 $objectId = $('#id_object_id');
|
||||
const $searchInput = $('#object_search_input');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
{% 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 %}
|
||||
<div class="container py-4">
|
||||
|
|
@ -42,10 +47,10 @@
|
|||
<form method="post" action="{% url 'documents:folder_share_add' folder.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="{{ share_form.user.id_for_label }}">{% translate "User" %}</label>
|
||||
{{ share_form.user }}
|
||||
{% if share_form.user.errors %}
|
||||
<div class="invalid-feedback d-block">{{ share_form.user.errors|join:', ' }}</div>
|
||||
<label class="form-label" for="{{ share_form.users.id_for_label }}">{% translate "Users" %}</label>
|
||||
{{ share_form.users }}
|
||||
{% if share_form.users.errors %}
|
||||
<div class="invalid-feedback d-block">{{ share_form.users.errors|join:', ' }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
|
@ -63,7 +68,7 @@
|
|||
<ul class="list-group list-group-flush">
|
||||
{% for share in shares %}
|
||||
<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">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
|
|
@ -144,4 +149,21 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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)
|
||||
|
||||
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)
|
||||
existing_folder_ids = list(document.folders.values_list("pk", flat=True))
|
||||
accessible_folders = filter_folders_for_user(
|
||||
DocumentFolder.objects.all(),
|
||||
DocumentFolder.objects.prefetch_related("parent_folders"),
|
||||
self.request.user,
|
||||
required_permission=ManagedDocument.PERMISSION_VIEW,
|
||||
)
|
||||
context["available_folders"] = accessible_folders.exclude(pk__in=existing_folder_ids).order_by("name")
|
||||
context["all_accessible_folders"] = accessible_folders.order_by("name")
|
||||
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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue