Nobody has started this yet — be first.
Business impact
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.
Problem
paginate_query(query, page, per_page, max_per_page) is supposed to cap an oversized per_page down to max_per_page, while leaving a smaller per_page alone. Instead it forces per_page up to max_per_page whenever the caller's value is smaller, and, as a direct consequence of the same bug, also stops capping oversized values down at all.
Current behavior
A request for per_page=2 against a form with 5 submissions comes back with all 5 items instead of 2, and a request for per_page=99999 is honored as-is instead of being capped to 100.
Expected behavior
per_page=2 with max_per_page=100 -> the response's per_page is 2, and at most 2 items come back. per_page=99999 with max_per_page=100 -> the response's per_page is capped down to 100.
Steps to reproduce
cd flask/feedback_service export FLASK_ENV=testing FLASK_ENV=testing .venv/bin/python -m pytest ticket_tests/test_ticket_02_pagination_ignores_small_per_page.py -v
Why this matters
min() and max() are easy to swap by accident (they are one character apart in most people's typing rhythm, and both "sound" plausible for "a cap"), and unlike many off-by-ones, swapping them does not produce an obviously-broken result for the common case -- a page of default size (20) is still well under the cap of 100, so this bug is invisible unless someone specifically requests a small per_page. That is exactly the kind of bug that survives casual manual testing and ships to production.
Suggested approach
paginate_query takes the caller's requested per_page and the server's configured ceiling, and is supposed to produce "whichever is smaller." Re-derive that logic from the plain-English description above rather than trusting the existing line -- and once fixed, check both directions (a small request, and an oversized request) rather than just the one you originally noticed.
Acceptance criteria
Verification
cd flask/feedback_service && FLASK_ENV=testing .venv/bin/python -m pytest ticket_tests/test_ticket_02_pagination_ignores_small_per_page.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 fix/pagination-ignores-small-per-pageFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/pagination-ignores-small-per-pageSubmit 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 1
Fix a bug
Read existing behaviour, correct it.