407 lines
No EOL
14 KiB
Python
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 |