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