Aller au contenu

Forms

Status: Placeholder — to be developed. Last reviewed:Reference structural sibling: guidelines/ui/forms.md (close cousin — that file covers visual layout, this one covers Python construction).

Scope (when this guideline lands)

Django form / ModelForm conventions: Crispy Forms vs manual rendering (skeleton uses manual — document why), where validation lives (form clean_* vs model clean vs service), SearchSelect widget usage on form fields, file-upload form patterns, multi-step / multi-fieldset forms, how forms wire into views.

This complements guidelines/ui/forms.md (which covers visual layout); this file covers Python-side form construction.

Out of scope (cross-refs)

  • Form template rendering (fieldsets, labels, validation feedback slot, character counters, JS hooks) → guidelines/ui/forms.md.
  • SearchSelect widget contract (when to use, sort/filter behaviour, disabled-choices) → guidelines/ui/dropdowns.md (placeholder).
  • Model validation (model clean(), field constraints) → guidelines/backend/models.md (placeholder).
  • i18n of form labels and errorsguidelines/i18n/translation-rules.md.

Sources to mine when writing this

  • apps/*/forms.py across the project — survey the patterns in use. apps/register/forms.py is rich (HolderForm with conditional fieldsets); apps/imports/forms.py covers file upload.
  • templates/skeletons/skeleton_form.html — the rendering convention forms must support.
  • apps/core/widgets.pySearchSelect widget contract.
  • The "more contextual forms" rework (holder forms in commit history) — a reference for conditional fieldsets / dynamic field visibility.

Starter hard rules to investigate

  1. ModelForm over Form when wrapping a model. Use Form only for non-model inputs (search bars, filter forms, login).
  2. Validation lives in form clean_FIELD for field-level checks, model clean() for cross-field constraints that must hold at the DB layer too.
  3. Never override save() to do business logic — keep save() as Django's default; put post-save side effects in the view (call a service) or a model save() signal.
  4. SearchSelect for any choice field with > 8 options — see guidelines/ui/dropdowns.md (placeholder).
  5. Use gettext_lazy for label, help_text, error_messages — these are evaluated at class definition (import time), so eager gettext is wrong.

Decision points to settle

  1. Where validation lives: form vs model vs service. The codebase has all three; pick a hierarchy with explicit boundaries.
  2. File upload patterns: max size, allowed types, server-side validation. Survey current importers + finance attachments.
  3. Multi-step / wizard forms: do any exist? (Django-formtools or hand-rolled?) If yes, document; if no, declare out-of-scope.
  4. Form field initial values: initial= in form vs get_initial() in view — pick the canonical placement.

Known deviations to look for during writing

  • Forms with def save(self): doing more than super().save().
  • Forms importing gettext (eager) instead of gettext_lazy.
  • Plain Form subclasses that wrap a model (should be ModelForm).
  • Hand-rolled CSV parsing in form clean() instead of using importers.

If found, file as roadmap/backlog/backend-forms-drift-2026-MM.md.