# Add CSV Export to a Wagtail Admin

Yes. An agent adds CSV and XLSX export to your Wagtail snippet listings and a page report, writes tests, runs the suite and lint, and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/add-csv-export/wagtail/), in a Project that has your repository.

## What the agent does

The agent starts from the Linear issue, which says which content editors need to export, then reads how the admin is built. In a site like wagtail/bakerydemo, `requirements/base.txt` pins `wagtail>=8.0,<8.1` on `Django>=6.0,<6.1`, `runtime.txt` says Python 3.12.1, and the admin is customised in `wagtail_hooks.py` files rather than with `@register_snippet` on models. `bakerydemo/breads/wagtail_hooks.py` registers a `BreadMenuGroup` holding a `BreadPageListingViewSet` for `BreadPage`, with a `BreadPageFilterSet` on `origin`, `bread_type` and `ingredients` and a custom `origin` column in place of `parent`, plus `BreadIngredientSnippetViewSet`, `BreadTypeSnippetViewSet` and a `CountryModelViewSet` built on `ModelViewSet`. None of them exports anything.

One viewset in the same codebase already does, and it is the pattern to copy. `bakerydemo/base/wagtail_hooks.py` gives `PersonViewSet` a `list_export = ("first_name", "last_name", "job_title")`, which is all a `SnippetViewSet` or `ModelViewSet` needs for Wagtail to add the download buttons to its index view and answer `?export=csv` and `?export=xlsx`. So the agent adds `list_export` to the three bread viewsets: `name` and `live` for `BreadIngredient`, which carries `DraftStateMixin`, `title` for `BreadType`, and `title` and `sort_order` for `Country`, keeping the existing `list_display`, filters and `inspect_view_enabled` settings as they are.

`BreadPage` is a page, not a snippet, and the issue wants one file of all breads with their origin, type and ingredients, so the agent adds a report. It writes a `BreadPageReportView` on Wagtail's `PageReportView` in `bakerydemo/breads/views.py`, whose queryset is live `BreadPage` objects with `select_related` on `origin` and `bread_type` and `prefetch_related` on `ingredients`, with `list_export` naming `title`, `origin`, `bread_type`, a method that joins the ordered ingredient names, and `url`, and `export_headings` for the human labels. It registers the view with `register_admin_urls` and adds it to the Reports menu with `register_reports_menu_item` in `breads/wagtail_hooks.py`, which is the documented pair of hooks for a custom report, so the download buttons and the `export` query parameter come from the same mixin the snippet listings use.

Tests follow `bakerydemo/base/tests/test_home_page.py`: a `WagtailPageTestCase` that creates a superuser in `setUp`, logs in with the test client and asserts on responses. The new `breads/tests` module creates a `Country`, a `BreadType`, two `BreadIngredient` rows and a `BreadPage` under a `BreadsIndexPage`, then requests the ingredient listing and the report with `?export=csv`, asserting a `text/csv` content type, the heading row and one data row with the joined ingredients, and once more with `?export=xlsx` for the content type. It runs what `.github/workflows/ci.yml` runs with `DJANGO_SETTINGS_MODULE=bakerydemo.settings.test`: `./manage.py check`, `./manage.py makemigrations --check --noinput`, `./manage.py test`, and `make lint-server`, which is `ruff format --check`, `ruff check`, `curlylint` and `djhtml --check`, then opens the pull request.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add CSV and XLSX export to this Wagtail admin as the linked issue asks. Read requirements/base.txt, bakerydemo/breads/models.py, bakerydemo/breads/wagtail_hooks.py and bakerydemo/base/wagtail_hooks.py, where PersonViewSet already uses list_export. Add list_export to BreadIngredientSnippetViewSet, BreadTypeSnippetViewSet and CountryModelViewSet with the fields editors would expect. Add a report view on PageReportView in bakerydemo/breads/views.py listing live BreadPage objects with title, origin, bread type, the ordered ingredient names joined, and url, with list_export and export_headings, and register it with register_admin_urls and register_reports_menu_item. Write tests in the style of bakerydemo/base/tests/test_home_page.py that create the models, log in as a superuser, request a snippet listing and the report with export=csv and export=xlsx, and assert the content type, the headings and a data row. Run ./manage.py check, ./manage.py makemigrations --check --noinput and ./manage.py test with DJANGO_SETTINGS_MODULE=bakerydemo.settings.test, then make lint-server. Open a pull request that links the issue, lists the files and quotes the test and lint output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch that links the Linear issue. It changes `bakerydemo/breads/wagtail_hooks.py`, adds `bakerydemo/breads/views.py` and a `bakerydemo/breads/tests` module, and touches no model and no migration, which `makemigrations --check` confirms in the quoted output. The description lists each viewset and the fields it now exports, describes the report's columns and where it appears in the Reports menu, and quotes the `manage.py test` and `make lint-server` output from the sandbox.

The session read names the repository and the pull request URL. The transcript at `GET /o/{org}/sessions/{id}/messages` keeps every command: the install from `requirements/development.txt`, the Django checks, each test run including any failing first attempt, and the lint. A reviewer can see what the agent ran without checking the branch out.

Before merging, a reviewer loads the Reports menu on a review deploy and downloads the file once, checks that the ingredient join respects the `ordered_ingredients` order the model defines, and confirms the report queryset uses `live()` so drafts stay out of the export. If editors also need the export on the `BreadPage` listing itself, that is a follow-up on the same session.

## Run it on every Linear issue labelled export

Add an automation to the Project with this prompt and a `linear` trigger on `linear.issue.labeled` with `export` in its labels. Each labelled issue starts one run in the Project's repository, and the trigger adds the issue after the prompt, so the agent reads which models and columns editors asked for from the issue and the prompt text stays as written. A label added by a Linear app integration never fires it, and the automation starts at most `max_runs_per_hour` runs in an hour.

From your own server, the same run starts with this request and `https://linear.app/<workspace>/issue/<ID>` added to `resources`, read through your Linear connection. Give the key a webhook URL and Rush POSTs `run.completed` with `data.session_url` when the turn ends, so a handler can post the pull request link back on the issue.

## Why run it on Rush?

This job is small in code and large in setup: a virtualenv from `requirements/development.txt`, a database for the test suite, a logged-in admin to click the download button, and three linters that CI runs. The agent does all of it in its own sandbox and loads the report in its browser before opening the pull request, so the first review sees a file that was downloaded, not a diff that should produce one.

The proof is the pull request plus the transcript. The exact `manage.py test` output, the `makemigrations --check` result that shows no schema change and the lint run are kept on the session, and several export issues can run at once, each in its own sandbox with its own pull request.

## Frequently asked questions

### Does this add a migration?

No. `list_export` on a viewset and a report view are admin configuration, not model fields, so `./manage.py makemigrations --check --noinput` passes unchanged and the pull request says so with the command's output.

### Why a report for BreadPage instead of exporting the page listing?

The bread pages are listed through a `PageListingViewSet`, and the issue asks for one file with related data joined in, the origin, type and ingredients. A `PageReportView` with `list_export` is the documented way to ship exactly that set of columns under the Reports menu, using the same export mixin the snippet listings use.

### Will drafts or unpublished breads be in the file?

Not from the report, whose queryset is filtered with `live()`. The ingredient listing exports whatever the index shows, and `BreadIngredient` has `DraftStateMixin`, so its export includes a `live` column so editors can tell the two apart.

### What does it run before opening the pull request?

The same steps as `.github/workflows/ci.yml`: `./manage.py check`, `./manage.py makemigrations --check --noinput` and `./manage.py test` with the test settings, then `make lint-server`, which runs `ruff format --check`, `ruff check`, `curlylint` and `djhtml --check`. The output of each is quoted in the description.
