Files
RosterChief/formbuilder/models.py
Bernard Siebens eace903f05 chore: rebrand ClubManager -> RosterChief, add lucide icons
The clubmanager.app domain was taken, so the platform is now RosterChief
(rosterchief.app). Renames the Django project package clubmanager/ ->
rosterchief/ (git tracks it as a move, so history follows), every
`from rosterchief.base import ...`, the settings/wsgi/asgi module paths,
env vars (ROSTERCHIEF_BASE_DOMAIN / ROSTERCHIEF_RP_NAME), the MFA adapter
(RosterChiefMFAAdapter), brand text, and the docs.

Two things were deliberately NOT swept:
- club.models.ClubManager stays: it is the Django manager *for Club*, not the
  brand. A blind rename would have silently broken it.
- Migrations are untouched (history is not rewritten). The only reference was a
  cosmetic help_text, so a normal AlterField migration carries the new domain.

Note the WebAuthn RP ID is the base domain, so moving to rosterchief.app
cryptographically invalidates any passkey enrolled under the old one; they
cannot be migrated and must be re-enrolled. Nothing is in production, so the
real cost is zero.

Add django-lucide (from bsiebens/lucide) for icons: the theme toggle now swaps
sun/moon against the effective theme, and the control panel gets icons on its
tabs, actions and stat groups. Its classifiers stop at Django 5.0, but that is
stale metadata — verified rendering on Django 6 / Python 3.14.

Also add formbuilder, shop and controlpanel to ruff's known-first-party list,
which had drifted behind the apps that landed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:42:20 +02:00

108 lines
4.2 KiB
Python

from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import UniqueConstraint
from django.utils.translation import gettext_lazy as _
from members.models import Member
from rosterchief.base import ClubScopedModel, UUIDModel, unique_slugify
class Form(ClubScopedModel):
title = models.CharField(_("title"), max_length=255)
slug = models.SlugField(_("slug"), blank=True)
description = models.TextField(_("description"), blank=True)
slug_source = "title"
is_active = models.BooleanField(_("is active?"), default=True)
login_required = models.BooleanField(_("login required?"), default=False)
opens_at = models.DateTimeField(_("opens at"), blank=True, null=True)
closes_at = models.DateTimeField(_("closes at"), blank=True, null=True)
max_submissions_per_user = models.PositiveIntegerField(_("max submissions per user"), blank=True, null=True)
class Meta:
verbose_name = _("form")
verbose_name_plural = _("forms")
ordering = ["title"]
constraints = [
UniqueConstraint(fields=["club", "slug"], name="unique_form_slug_per_club"),
]
def __str__(self):
return self.title
class Field(UUIDModel):
class FieldType(models.TextChoices):
TEXT = "text", _("text")
TEXTAREA = "textarea", _("textarea")
NUMBER = "number", _("number")
EMAIL = "email", _("email")
DATE = "date", _("date")
CHOICE = "choice", _("choice")
MULTICHOICE = "multichoice", _("multichoice")
CHECKBOX = "checkbox", _("checkbox")
FILE = "file", _("file")
form = models.ForeignKey(Form, on_delete=models.CASCADE, related_name="fields", verbose_name=_("form"))
key = models.SlugField(_("key"), blank=True)
label = models.CharField(_("label"), max_length=255)
field_type = models.CharField(_("field type"), max_length=255, choices=FieldType.choices, default=FieldType.TEXT)
required = models.BooleanField(_("required?"), default=True)
help_text = models.TextField(_("help text"), blank=True)
order = models.PositiveIntegerField(_("order"), default=0)
is_active = models.BooleanField(_("is active?"), default=True)
options = models.JSONField(_("options"), blank=True, null=True)
class Meta:
verbose_name = _("field")
verbose_name_plural = _("fields")
ordering = ["form", "order"]
constraints = [
UniqueConstraint(fields=["form", "key"], name="unique_field_key_per_form"),
]
def save(self, *args, **kwargs):
if not self.key:
self.key = unique_slugify(self, self.label, slug_field="key", scope={"form": self.form})
super().save(*args, **kwargs)
def __str__(self):
return self.label
class Submission(UUIDModel):
form = models.ForeignKey(Form, on_delete=models.CASCADE, related_name="submissions", verbose_name=_("form"))
member = models.ForeignKey(Member, on_delete=models.SET_NULL, related_name="submissions", verbose_name=_("member"), blank=True, null=True)
submitted_at = models.DateTimeField(_("submitted at"), auto_now_add=True)
class Meta:
verbose_name = _("submission")
verbose_name_plural = _("submissions")
ordering = ["-submitted_at"]
def __str__(self):
return f"{self.form} - {self.member}"
class Answer(UUIDModel):
submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name="answers", verbose_name=_("submission"))
field = models.ForeignKey(Field, on_delete=models.PROTECT, related_name="answers", verbose_name=_("field"))
value = models.JSONField(_("value"), blank=True, null=True)
class Meta:
verbose_name = _("answer")
verbose_name_plural = _("answers")
ordering = ["submission", "field"]
constraints = [
UniqueConstraint(fields=["submission", "field"], name="unique_answer_per_field_per_submission"),
]
def __str__(self):
return f"{self.submission} - {self.field}"
def clean(self):
if self.field_id and self.submission_id and self.field.form_id != self.submission.form_id:
raise ValidationError({"field": _("Must belong to the same form as the submission.")})