From d43ca0cfa822c71155143ad1c5217f08b2da059f Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Mon, 13 Jul 2026 14:42:17 +0200 Subject: [PATCH] feat(formbuilder): answers must belong to the submission's form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Answer's field could point at a field of a *different* form than its submission — and since forms are club-scoped, across clubs too. Validate field.form == submission.form in clean(). Co-Authored-By: Claude Opus 4.8 --- formbuilder/models.py | 5 +++++ formbuilder/tests.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/formbuilder/models.py b/formbuilder/models.py index 9f8237e..37601bc 100644 --- a/formbuilder/models.py +++ b/formbuilder/models.py @@ -1,3 +1,4 @@ +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 _ @@ -100,3 +101,7 @@ class Answer(UUIDModel): 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.")}) diff --git a/formbuilder/tests.py b/formbuilder/tests.py index d0610ca..cfa884a 100644 --- a/formbuilder/tests.py +++ b/formbuilder/tests.py @@ -1,6 +1,7 @@ from datetime import timedelta from django import forms +from django.core.exceptions import ValidationError from django.db import IntegrityError from django.db.models import ProtectedError from django.test import TestCase @@ -329,3 +330,20 @@ class FormReportTests(FormbuilderTestBase): self.assertEqual(report.count, 0) self.assertEqual(report.rows, []) + + +class AnswerCleanTests(FormbuilderTestBase): + def test_rejects_field_from_another_form(self): + other_form = Form.objects.create(club=self.club, title="Other", slug="other") + other_field = Field.objects.create(form=other_form, key="x", label="X", order=1) + submission = Submission.objects.create(form=self.form, member=self.member) + answer = Answer(submission=submission, field=other_field, value="v") + + with self.assertRaises(ValidationError) as ctx: + answer.full_clean() + self.assertIn("field", ctx.exception.error_dict) + + def test_accepts_field_from_the_submissions_form(self): + submission = Submission.objects.create(form=self.form, member=self.member) + + Answer(submission=submission, field=self.name, value="v").full_clean()