feat(formbuilder): answers must belong to the submission's form

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 14:42:17 +02:00
parent 22d971d48c
commit d43ca0cfa8
2 changed files with 23 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import UniqueConstraint from django.db.models import UniqueConstraint
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -100,3 +101,7 @@ class Answer(UUIDModel):
def __str__(self): def __str__(self):
return f"{self.submission} - {self.field}" 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.")})

View File

@@ -1,6 +1,7 @@
from datetime import timedelta from datetime import timedelta
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.test import TestCase from django.test import TestCase
@@ -329,3 +330,20 @@ class FormReportTests(FormbuilderTestBase):
self.assertEqual(report.count, 0) self.assertEqual(report.count, 0)
self.assertEqual(report.rows, []) 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()