Let a user download the report they are viewing as a CSV, with the same filters applied. The exported rows always match the rows on screen.
The export reuses the table's own query, so the file can never drift from what is on screen.
Backend first (the endpoint and serializer), then the button wires to it.
GET /reports/:id/export reuses build_report_query() — the table's own query.
rows_to_csv() using the stdlib csv module. Quotes commas, newlines, unicode.
An ExportButton by the filter bar. Spinner while in flight; disabled on empty results.
Pass the identical filter query string the table already builds, so the two never diverge.
Unit-test the serializer edge cases; integration-test that export row count equals the table's.
Everything else has a safe default. This one changes what gets built.
Very large reports could outrun the 30-second request timeout. How do we handle them?
Most reports are a few hundred rows and stream in under a second. A few have tens of thousands. Streaming a huge one synchronously from rows_to_csv() risks holding the request open past the gateway timeout. The endpoint URL is stable either way, so a background path can be added later behind the same URL.
rows_to_csv: commas, quotes, newlines, empty, unicode.text/csv + attachment filename.