A Flask "mini Google Forms" API whose real subject is everywhere dynamic, per-form data collides with a service that still has to validate, notify, export, and store files safely -- JSON(B)-backed dynamic schemas, fail-closed table-driven validation, Celery-backed async email, and a file-upload path that has to stay correct across a database rollback.
Nobody has started this yet — be first.
Field names at exactly the length limit are wrongly rejected
An owner trying to create a form with a field name that happens to be exactly 100 characters long gets a confusing rejection -- "'name' exceeds 100 characters" -- even though the name does not exceed 100 characters, it equals it. This is a paper-cut for a human typing a long field name by hand, but it is a sharper edge for anyone integrating programmatically: a script that trims field names to the documented 100-character limit before submitting will get inconsistent 422s depending on whether the trimmed name happens to land at exactly 100 characters, with no obvious explanation in the error message for why an apparently within-limit name was rejected. Low severity, but exactly the kind of thing that generates a confused support ticket ("your docs say 100, I sent 100, it failed") that costs real time to triage.
About this project
An authenticated form owner defines a form with an arbitrary, per-form set of fields (text/number/choice/multichoice/file); the public submits responses to it with no login required; the owner gets an async email notification per submission and can pull responses back out as a paginated list, a streamed CSV export, or a chart-ready analytics summary. Because a form's schema is genuinely per-form, Form.fields and Submission.answers are stored as JSON(B) rather than fixed columns, and every submitted answer is validated at request time against that specific form's own stored field list -- never a hardcoded schema, and never a blind pass-through of whatever JSON shows up.
Three engineering problems sit at the center. First, dynamic-schema validation done safely: the table-driven validators in walk a fixed set of Python branches keyed by each field's declared , and deliberately never , , or dynamically import anything derived from form data. Second, a public submission endpoint that is both a spam target and a slow-downstream problem: it is rate-limited via Flask-Limiter (Redis-backed, configured to fail rather than take the whole public form down if Redis hiccups), and the notification email is pushed onto a Celery task via with and jittered backoff, so a slow or dead SMTP server never blocks the HTTP response and never retries forever. Third, and hardest, a transaction boundary that spans two different systems: a validated file upload is written to disk the moment its own checks pass, but that write is not part of any SQL transaction -- so a later field in the same multi-file submission failing validation, and the resulting , correctly undoes the database rows while leaving the already-written file behind forever unless the request handler explicitly tracks and undoes it too.
Pagination cap forces every page up to the max instead of capping large requests down
GET /api/forms/<id>/submissions is the endpoint an owner's dashboard calls to show a page of recent responses. A well-behaved dashboard asks for a small page (per_page=10 or per_page=20, say, to keep a widget snappy) and expects to get back roughly that many rows. Right now, any per_page smaller than the server's cap (PAGINATION_MAX_PER_PAGE=100) is silently ignored and the full 100-row cap is used instead. For a form with thousands of submissions, that means every "give me a small page" request actually downloads up to 100 full submission objects (including nested attachment metadata) -- extra database load on every dashboard paint, slower page loads for the owner (especially on mobile, where this kind of dashboard is often checked), and wasted bandwidth for something that was supposed to be a lightweight, paginated call. It also silently breaks any owner-facing "results per page" UI control, since the server ignores whatever smaller value the UI sends.
app/blueprints/forms/validators.pytypeevalexec.delay()autoretry_fordb.session.rollback()Seven tickets are deliberately worked into an otherwise fully tested Flask codebase (56 passing tests before injection): four are real defects -- three deliberately injected (an off-by-one boundary check, a swapped min/max in a pagination cap, and a classic argument-order mistake in a password check that causes a total, silent login outage) and one found as-is during code review (the file-upload/rollback leak) -- and the rest are enhancements with the smallest possible compile-only stub already wired in, including a real CSV/spreadsheet-formula-injection security hardening ticket. Every ticket has a dedicated pytest test that goes green when the fix or feature is correct.
workercelery -A celery_worker.celery workerdocker compose -f docker-compose.prod.yml up --buildSECRET_KEYJWT_SECRET_KEY