Nobody has started this yet — be first.
Business impact
Owners of high-volume forms (think: an in-product feedback widget collecting hundreds of responses a day) currently have exactly one way to look at their submissions: the full, unfiltered, newest-first list (or the full CSV export). There is no way to ask "just this week's feedback" or "everything since our last release." A customer-support lead who wants to send a weekly digest, or triage a spike in negative feedback right after a launch, has to export everything and filter it client-side in a spreadsheet every single time -- slow, wasteful, and a manual step that is easy to skip, meaning trends get missed. This is a small, common, expected feature for anything calling itself a "form responses" product; its absence is a real usability gap, not just a nice-to-have.
Problem
GET /api/forms/<id>/submissions supports page and per_page only. There is a placeholder helper, app/utils.py::filter_by_date_range, that the route is already wired up to call whenever a since and/or until query parameter is present -- but the helper itself is not implemented yet: it unconditionally raises NotImplementedError. So right now, any request that includes since or until blows up instead of filtering anything.
Current behavior
GET /api/forms/<id>/submissions?since=... (or ?until=...) returns a 500 with NotImplementedError: filter_by_date_range: date-range filtering not implemented yet, instead of a filtered list.
Expected behavior
GET /api/forms/<id>/submissions?since=<ISO8601>&until=<ISO8601> returns only submissions with submitted_at inside [since, until] (inclusive on both ends). Either bound may be omitted (an open-ended range on that side). Neither bound present -> current behavior (no filtering) is unchanged. A malformed since/until value (not a parseable ISO 8601 datetime) returns a clean 422 with a field_errors body -- consistent with every other validation failure in this API -- not a 500.
Steps to reproduce
cd flask/feedback_service export FLASK_ENV=testing FLASK_ENV=testing .venv/bin/python -m pytest ticket_tests/test_ticket_04_submission_date_range_filter.py -v
since to produce a clean 422, not a 500.Why this matters
This is this project's dynamic-schema-and-validation pattern applied to a much smaller, more contained problem: parse untrusted input (a query string), fail closed on anything that does not parse, and only then touch the database. It is also a good place to practice building a SQLAlchemy filter on top of an existing Query object that has already been through one .filter_by(...) and .order_by(...) call (app/blueprints/submissions/routes.py::list_submissions) -- the function needs to compose with what is already there, not replace it.
Suggested approach
Look at how Submission.submitted_at is stored (see app/models.py) -- it is a timezone-aware DateTime. Python's standard library has a built-in ISO 8601 parser on the datetime class itself; you should not need a third-party dependency for this. Decide what "malformed input" means precisely (an unparseable string is the main case to handle) and where that should turn into a 422 -- look at how other validation failures in this codebase build their field_errors response shape (app/blueprints/submissions/routes.py::create_submission has several examples) and match that convention rather than inventing a new one.
Acceptance criteria
Verification
cd flask/feedback_service && FLASK_ENV=testing .venv/bin/python -m pytest ticket_tests/test_ticket_04_submission_date_range_filter.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b feat/submission-date-range-filterFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/submission-date-range-filterSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inweb and worker share the same image and only differ in their command: -- web runs flask db upgrade && flask run, worker runs celery -A celery_worker.celery worker. For a prod-style stack (built image, gunicorn, no bind mount) use docker compose -f docker-compose.prod.yml up --build instead; it refuses to start without SECRET_KEY/JWT_SECRET_KEY actually set in the environment.
cd flask/feedback_service
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # defaults are fine for local dev as-is
export FLASK_APP=wsgi.py FLASK_ENV=development
flask db upgrade # applies migrations to a fresh SQLite DB under instance/
# terminal 1
flask run --port 5000
# terminal 2 (needs a local redis-server running — broker for both Celery and the rate limiter)
celery -A celery_worker.celery worker --loglevel=info
Each ticket in ticket_tests/tickets/0N_*.md names one dedicated test file under ticket_tests/:
cd flask/feedback_service
export FLASK_ENV=testing
.venv/bin/python -m pytest ticket_tests/test_ticket_0N_*.py -v # a single ticket
./ticket_tests/run_tickets.sh # all 7, PASS/FAIL scoreboard
TestingConfig runs Celery tasks eagerly (CELERY_TASK_ALWAYS_EAGER=True, no broker needed) and uses a real, per-test-flushed Redis DB for the rate limiter when Redis is reachable, falling back to Flask-Limiter's in-memory backend otherwise -- either way the real Flask-Limiter code path runs, nothing about rate limiting is mocked. Note ticket_tests/ is not collected by a plain pytest -q from the project root (pytest.ini pins testpaths = tests), so the project's own suite and this practice suite stay independent and must be run explicitly.
Level 2
Implement a feature
Extend the system within its own patterns.