feat(formbuilder): fix tenancy bugs, add admin + services
Fix multi-tenancy/integrity bugs in the models: Form.slug and Field.key were globally unique (unique=True), so two clubs couldn't reuse a form slug and two forms couldn't reuse a field key — make slug unique per club (constraint already present) and key unique per form. Add a (submission, field) uniqueness constraint on Answer. Register all four models in the admin (Field inline on Form, Answer inline on Submission) and add formbuilder to the admin registration smoke test. Add a service layer: - submit_form(form, member, data): enforces is_active / login_required / open window / max_submissions, validates required + choice fields, and writes a Submission with Answers atomically (FormSubmissionError carries per-field errors). - build_form(form): a live django.forms.Form built from a Form's active fields, mapping each FieldType to the matching form field. - form_report(form): a tabular overview of every submission's answers plus per-value tallies for choice-type fields. Full suite at 100% coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
46
formbuilder/services/reporting.py
Normal file
46
formbuilder/services/reporting.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Build a tabular overview of all answers for a Form."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from formbuilder.models import Field
|
||||
|
||||
CHOICE_TYPES = {Field.FieldType.CHOICE, Field.FieldType.MULTICHOICE, Field.FieldType.CHECKBOX}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReportRow:
|
||||
submission: object
|
||||
values: dict # field id -> answer value
|
||||
|
||||
|
||||
@dataclass
|
||||
class FormReport:
|
||||
form: object
|
||||
columns: list # Field instances, in display order
|
||||
rows: list # ReportRow, newest submission first
|
||||
summaries: dict # field id -> {stringified value: count} for choice-like fields
|
||||
count: int
|
||||
|
||||
|
||||
def form_report(form):
|
||||
"""Return a FormReport: one column per field, one row per submission."""
|
||||
columns = list(form.fields.order_by("order"))
|
||||
summaries = {column.id: {} for column in columns if column.field_type in CHOICE_TYPES}
|
||||
|
||||
rows = []
|
||||
submissions = form.submissions.select_related("member").prefetch_related("answers")
|
||||
for submission in submissions:
|
||||
values = {answer.field_id: answer.value for answer in submission.answers.all()}
|
||||
rows.append(ReportRow(submission=submission, values=values))
|
||||
for field_id, bucket in summaries.items():
|
||||
if field_id in values:
|
||||
_tally(bucket, values[field_id])
|
||||
|
||||
return FormReport(form=form, columns=columns, rows=rows, summaries=summaries, count=len(rows))
|
||||
|
||||
|
||||
def _tally(bucket, value):
|
||||
items = value if isinstance(value, list) else [value]
|
||||
for item in items:
|
||||
key = str(item)
|
||||
bucket[key] = bucket.get(key, 0) + 1
|
||||
Reference in New Issue
Block a user