loko/streetup/documents/forms.py
2026-07-22 14:48:40 +02:00

407 lines
No EOL
14 KiB
Python

from django import forms
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from .models import (
DocumentAccessLink,
DocumentAttachment,
DocumentFolder,
DocumentFolderAccessLink,
DocumentFolderShare,
DocumentShare,
DocumentTag,
DocumentType,
DocumentValidation,
DocumentVersion,
ManagedDocument,
)
from common.models import Thematic
User = get_user_model()
class TagField(forms.CharField):
def to_python(self, value):
if not value:
return []
if isinstance(value, list):
return value
return [item.strip() for item in value.split(",") if item.strip()]
class ManagedDocumentForm(forms.ModelForm):
file = forms.FileField(label=_("File"))
comment = forms.CharField(label=_("Comment"), required=False, widget=forms.Textarea(attrs={"rows": 2}))
tags_input = TagField(label=_("Tags"), required=False, help_text=_("Separate tags with commas."))
class Meta:
model = ManagedDocument
fields = ["title", "description", "types", "tags", "folders"]
widgets = {
"description": forms.Textarea(attrs={"rows": 3}),
"types": forms.SelectMultiple(attrs={"class": "form-select"}),
"tags": forms.SelectMultiple(attrs={"class": "form-select"}),
"folders": forms.SelectMultiple(attrs={"class": "form-select"}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None)
initial_folder = kwargs.pop("initial_folder", None)
super().__init__(*args, **kwargs)
# Limiter les folders accessibles à l'utilisateur
if user:
from .permissions import filter_folders_for_user
accessible_folders = filter_folders_for_user(
DocumentFolder.objects.all(),
user,
required_permission=ManagedDocument.PERMISSION_VIEW
)
self.fields["folders"].queryset = accessible_folders.order_by("name")
# Pré-sélectionner le folder initial si fourni
if initial_folder and not self.instance.pk:
self.fields["folders"].initial = [initial_folder.pk]
self.fields["title"].widget.attrs.setdefault("class", "form-control")
self.fields["description"].widget.attrs.setdefault("class", "form-control")
self.fields["types"].widget.attrs.setdefault("class", "form-select")
self.fields["tags"].widget.attrs.setdefault("class", "form-select")
self.fields["folders"].widget.attrs.setdefault("class", "form-select")
self.fields["tags_input"].widget.attrs.setdefault("class", "form-control")
self.fields["file"].widget.attrs.setdefault("class", "form-control")
self.fields["comment"].widget.attrs.setdefault("class", "form-control")
def save(self, user=None, commit=True):
tags_input = self.cleaned_data.pop("tags_input", [])
document = super().save(commit=False)
document.created_by = user
if commit:
document.save()
self.save_m2m()
# if tags_input:
# for tag_name in tags_input:
# tag, _ = DocumentTag.objects.get_or_create(
# slug=slugify(tag_name),
# defaults={"name_fr": tag_name, "name_nl": tag_name},
# )
# document.tags.add(tag)
file = self.cleaned_data["file"]
comment = self.cleaned_data.get("comment", "")
version = DocumentVersion(
document=document,
file=file,
comment=comment,
description=document.description,
uploaded_by=user,
)
version.save()
return document
class DocumentVersionForm(forms.ModelForm):
class Meta:
model = DocumentVersion
fields = ["file", "comment"]
widgets = {
"comment": forms.Textarea(attrs={"rows": 2}),
}
def save(self, commit=True, user=None, document=None):
version = super().save(commit=False)
if user:
version.uploaded_by = user
if document:
version.document = document
version.description = document.description
if commit:
version.save()
return version
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["file"].widget.attrs.setdefault("class", "form-control")
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"}),
}
def __init__(self, *args, **kwargs):
document = kwargs.pop("document", None)
super().__init__(*args, **kwargs)
if document:
self.fields["user"].queryset = User.objects.exclude(document_shares__document=document)
self.fields["user"].widget.attrs.setdefault("class", "form-select")
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
class DocumentFolderShareForm(forms.ModelForm):
class Meta:
model = DocumentFolderShare
fields = ["user", "permission"]
widgets = {
"permission": forms.Select(attrs={"class": "form-select"}),
}
def __init__(self, *args, **kwargs):
folder = kwargs.pop("folder", None)
super().__init__(*args, **kwargs)
if folder:
self.fields["user"].queryset = User.objects.exclude(
document_folder_shares__folder=folder
)
self.fields["user"].widget.attrs.setdefault("class", "form-select")
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
class DocumentValidationForm(forms.ModelForm):
class Meta:
model = DocumentValidation
fields = ["status", "comment"]
widgets = {
"status": forms.Select(attrs={"class": "form-select"}),
"comment": forms.Textarea(attrs={"rows": 2}),
}
def save(self, user=None, commit=True):
validation = super().save(commit=False)
if user:
if validation.status == DocumentValidation.STATUS_APPROVED:
validation.approve(user, validation.comment)
return validation
if validation.status == DocumentValidation.STATUS_REJECTED:
validation.reject(user, validation.comment)
return validation
if commit:
validation.save()
return validation
class AttachmentForm(forms.ModelForm):
allowed_non_assets = {
("projects", "project"),
("interventions", "intervention"),
("observations", "observation"),
("controls", "control"),
}
content_type = forms.ModelChoiceField(
queryset=ContentType.objects.none(),
label=_("Linked object type"),
widget=forms.Select(attrs={"class": "form-select"}),
)
class Meta:
model = DocumentAttachment
fields = ["content_type", "object_id", "note"]
widgets = {
"object_id": forms.HiddenInput(),
"note": forms.TextInput(attrs={"class": "form-control"}),
}
def __init__(self, *args, **kwargs):
document = kwargs.pop("document", None)
super().__init__(*args, **kwargs)
from django.db.models import Q
# Build base queryset: assets or specific non-asset models
q_expr = Q(app_label="assets")
for app, model in self.allowed_non_assets:
q_expr |= Q(app_label=app, model=model)
queryset = ContentType.objects.filter(q_expr)
if document:
thematics = Thematic.objects.filter(document_folders__documents=document).distinct()
if thematics.exists():
queryset = queryset.filter(
~Q(app_label="assets") |
Q(asset_categories__thematic__in=thematics)
).distinct()
self.fields["content_type"].queryset = queryset.order_by("app_label", "model")
def clean(self):
cleaned = super().clean()
content_type = cleaned.get("content_type")
object_id = cleaned.get("object_id")
if content_type and object_id:
model_class = content_type.model_class()
if model_class is None:
raise forms.ValidationError(_("Unable to resolve the selected object type."))
if not model_class.objects.filter(pk=object_id).exists():
raise forms.ValidationError(_("The selected object does not exist."))
return cleaned
def save(self, commit=True, user=None, document=None):
attachment = super().save(commit=False)
if user and not attachment.attached_by:
attachment.attached_by = user
if document:
attachment.document = document
if commit:
attachment.save()
return attachment
class AccessLinkForm(forms.ModelForm):
class Meta:
model = DocumentAccessLink
fields = ["permission", "expires_at"]
widgets = {
"permission": forms.Select(attrs={"class": "form-select"}),
"expires_at": forms.DateTimeInput(attrs={"class": "form-control", "type": "datetime-local"}),
}
def save(self, commit=True, user=None, document=None):
link = super().save(commit=False)
if user and not link.created_by:
link.created_by = user
if document:
link.document = document
if commit:
link.save()
return link
class DocumentFolderAccessLinkForm(forms.ModelForm):
class Meta:
model = DocumentFolderAccessLink
fields = ["permission", "expires_at"]
widgets = {
"permission": forms.Select(attrs={"class": "form-select"}),
"expires_at": forms.DateTimeInput(
attrs={"class": "form-control", "type": "datetime-local"}
),
}
def save(self, commit=True, user=None, folder=None):
link = super().save(commit=False)
if user and not link.created_by:
link.created_by = user
if folder:
link.folder = folder
if commit:
link.save()
return link
class DocumentFolderForm(forms.ModelForm):
class Meta:
model = DocumentFolder
fields = [
"name",
"description",
"application_label",
"parent_folders",
"thematics",
"created_by",
]
widgets = {
"name": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"application_label": forms.TextInput(attrs={"class": "form-control"}),
"parent_folders": forms.SelectMultiple(attrs={"class": "form-select"}),
"thematics": forms.SelectMultiple(attrs={"class": "form-select"}),
"created_by": forms.Select(attrs={"class": "form-select"}),
}
def __init__(self, *args, **kwargs):
self.request_user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
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")
)
else:
self.fields["parent_folders"].queryset = DocumentFolder.objects.order_by("name")
if "thematics" in self.fields:
self.fields["thematics"].queryset = Thematic.objects.order_by("name_fr")
self.fields["thematics"].label = _("Thematics")
self.fields["thematics"].required = False
instance = getattr(self, "instance", None)
if not instance or not getattr(instance, "is_predefined", False):
self.fields.pop("thematics", None)
self.fields["created_by"].label = _("Owner")
self.fields["created_by"].required = False
if "created_by" in self.fields:
self.fields["created_by"].queryset = User.objects.order_by("username")
if not self._can_edit_owner():
self.fields.pop("created_by", None)
def _can_edit_owner(self):
user = self.request_user
if user is None or not getattr(user, "is_authenticated", False):
return False
if getattr(user, "is_superuser", False) or getattr(user, "is_staff", False):
return True
config = getattr(user, "config", None)
return bool(config and config.has_role("admin"))
def _get_descendant_ids(self, folder):
descendant_ids = set()
stack = [folder.pk]
while stack:
current_id = stack.pop()
if current_id in descendant_ids:
continue
descendant_ids.add(current_id)
child_ids = (
DocumentFolder.objects.filter(parent_folders__pk=current_id)
.values_list("pk", flat=True)
)
stack.extend(child_ids)
descendant_ids.discard(folder.pk)
return descendant_ids
def clean_parent_folders(self):
parents = self.cleaned_data.get("parent_folders")
instance = getattr(self, "instance", None)
if not instance or not instance.pk:
return parents
forbidden_ids = {instance.pk, *self._get_descendant_ids(instance)}
for parent in parents:
if parent.pk in forbidden_ids:
raise forms.ValidationError(
_("A folder cannot be attached to itself or one of its descendants."),
)
return parents