Captures: the 'discarded captures' link redirects to Listings, and nothing lists or restores a discarded capture #380

Closed
opened 2026-10-01 10:46:16 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, following the link the discard message offers and checking every place that lists captures or changes a capture's status.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at api/api.py:633-647 (list_captures) and jobs/pages.py:800-821 (expire_unconfirmed); the capture views, the routes, the Listings view, the templates, notifications/slow.py:106 and the two tests have not moved.

Related to #260 (closed), which made the discard message link to "the discarded captures" so that a wrong key press would be recoverable, and to #25 (closed), which replaced the capture list with a redirect to Listings. This issue adds that the link #260 wrote points at that redirect, so it never showed a discarded capture, and that no route puts one back. It also touches #336 (open): once the review page refuses decided captures, the last way back, a hand-typed review address, goes too.

The message says "Capture discarded. It is in the discarded captures." and links to /jobs/captures/?show=all. That address redirects to Listings and drops the query string. Listings shows pending captures only, the API lists pending captures only, and nothing in the interface sets a discarded capture back to pending. The recovery #260 promised does not exist, at the one moment somebody is looking for it. The same block on Listings shows and counts at most 20 pending captures.

  • Where:
    • jobs/capture_views.py:431-440 (discarded_link): url = reverse("jobs:capture_list") + "?show=all". Its docstring still says "the list can show what is not pending".
    • jobs/capture_views.py:443-460 (CaptureDiscardView) and :463-494 (CaptureDiscardSelectedView) send "Capture discarded. It is in {link}." and "Captures discarded: {count}. They are in {link}.".
    • jobs/urls.py:37-42: capture_list is RedirectView.as_view(url=reverse_lazy("listings:list"), permanent=False). RedirectView.query_string defaults to False, so ?show=all is dropped.
    • jobs/listing_views.py:132-136: the Listings page takes Capture.objects.for_user(...).filter(status=CaptureStatus.PENDING), cut to [:20]. templates/jobs/listing_list.html:78 counts pending_captures|length for the heading. api/api.py:631-645 (list_captures) filters on PENDING too.
    • jobs/urls.py:38-93: the capture routes are the redirect, create, review, discard, discard-selected, bind and the five page routes. None restores.
    • Unrouted and unreferenced in src, tests and the sibling repositories: jobs/capture_views.py:134-149 (CaptureListView, the view that had the show=all switch and paginates by 50), jobs/capture_views.py:497 (capture_list_url) and templates/jobs/capture_list.html, whose strings ("Pending only", "Show all", "Nothing waiting") are still in the catalogues.
    • templates/jobs/capture_list.html:40-57: even were the list routed again, a row gets Review and Discard only when capture.is_pending, a link only when capture.application is set, and otherwise the bare {{ capture.get_status_display }}. A discarded row has nothing to press, and a capture saved as a listing without an application shows its status with no link to capture.posting.
    • tests/test_table_caption_and_discard.py:30-48 asserts the text of the href and never requests it. tests/test_listings.py:169-170 asserts that jobs:capture_list answers 302 to listings:list, so the two tests between them pin the broken state.
  • What happens:
    1. Somebody triaging presses d on the wrong capture, or discards a mistaken selection, and follows the discarded captures.
    2. GET /jobs/captures/?show=all answers 302 to /listings/, which shows pending captures and listings. The discarded capture is on neither, and no control on any page lists discarded captures or restores one.
    3. The capture can be reached again only by typing /jobs/captures/<pk>/review/, which nothing shows, and which works only because the review page does not check the status (#336). Discarding also emptied its learning (jobs/capture_views.py:451, :480), and its kept page is removed POSTULO_CAPTURE_PAGE_KEEP_DAYS (30 by default) after it was captured (jobs/pages.py:828-849, expire_unconfirmed).
    4. Separately, with more than 20 captures waiting, Listings shows 20 and its heading says "20 captures waiting for review"; with 25 pending the heading read 20. The rest are reachable only through next on a review page. notifications/slow.py:106 sends the "Captured N postings" notification of a batch to this same address, so a batch of forty lands on a page that says twenty.
  • Why it matters: #260 let the fast discard key stay fast on the strength of the capture being recoverable. It is recoverable only in the database, the message says otherwise, and the test that guards it cannot notice. The count misleads anyone with a real backlog, which is the case #179 was built for.
  • Fix:
    • Give captures one home that can show what is not pending. Either route jobs:capture_list back to CaptureListView, or add a discarded-captures view reached from the capture block on Listings (for example ?captures=discarded) listing for_user() captures with status DISCARDED. Point discarded_link() at it and correct its docstring. notifications/slow.py:106 should land in the same place.
    • Add a jobs:capture_restore POST view that, under select_for_update(), moves a DISCARDED capture of the requesting person back to PENDING, refuses any other status, and answers 404 for another account's capture. Show a Put it back button on each discarded row. Land it with or before #336, so that the review page refusing decided captures does not remove the only way back.
    • On whichever list survives, link an accepted row to capture.posting when it has no application.
    • Delete whichever of CaptureListView, templates/jobs/capture_list.html and capture_list_url is left unused, and let the catalogues drop the strings.
    • Count pending captures with a separate .count() for the heading, and link to the full list when there are more than 20.

Done when

  • tests/test_table_caption_and_discard.py discards a capture, follows the link in the message, and asserts that the discarded capture's title and a restore control are on the page it reaches.
  • A test beside it posts the restore and asserts the capture is pending again (next_pending finds it), that restoring a capture that is not discarded is refused, and that another account's capture answers 404.
  • A test in tests/test_listings.py with 25 pending captures asserts the heading says 25 and that the page links to the full list.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, following the link the discard message offers and checking every place that lists captures or changes a capture's status. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `api/api.py:633-647` (`list_captures`) and `jobs/pages.py:800-821` (`expire_unconfirmed`); the capture views, the routes, the Listings view, the templates, `notifications/slow.py:106` and the two tests have not moved. Related to #260 (closed), which made the discard message link to "the discarded captures" so that a wrong key press would be recoverable, and to #25 (closed), which replaced the capture list with a redirect to Listings. This issue adds that the link #260 wrote points at that redirect, so it never showed a discarded capture, and that no route puts one back. It also touches #336 (open): once the review page refuses decided captures, the last way back, a hand-typed review address, goes too. The message says "Capture discarded. It is in the discarded captures." and links to `/jobs/captures/?show=all`. That address redirects to Listings and drops the query string. Listings shows pending captures only, the API lists pending captures only, and nothing in the interface sets a discarded capture back to pending. The recovery #260 promised does not exist, at the one moment somebody is looking for it. The same block on Listings shows and counts at most 20 pending captures. - **Where:** - `jobs/capture_views.py:431-440` (`discarded_link`): `url = reverse("jobs:capture_list") + "?show=all"`. Its docstring still says "the list can show what is not pending". - `jobs/capture_views.py:443-460` (`CaptureDiscardView`) and `:463-494` (`CaptureDiscardSelectedView`) send "Capture discarded. It is in {link}." and "Captures discarded: {count}. They are in {link}.". - `jobs/urls.py:37-42`: `capture_list` is `RedirectView.as_view(url=reverse_lazy("listings:list"), permanent=False)`. `RedirectView.query_string` defaults to `False`, so `?show=all` is dropped. - `jobs/listing_views.py:132-136`: the Listings page takes `Capture.objects.for_user(...).filter(status=CaptureStatus.PENDING)`, cut to `[:20]`. `templates/jobs/listing_list.html:78` counts `pending_captures|length` for the heading. `api/api.py:631-645` (`list_captures`) filters on `PENDING` too. - `jobs/urls.py:38-93`: the capture routes are the redirect, create, review, discard, discard-selected, bind and the five page routes. None restores. - Unrouted and unreferenced in `src`, `tests` and the sibling repositories: `jobs/capture_views.py:134-149` (`CaptureListView`, the view that had the `show=all` switch and paginates by 50), `jobs/capture_views.py:497` (`capture_list_url`) and `templates/jobs/capture_list.html`, whose strings ("Pending only", "Show all", "Nothing waiting") are still in the catalogues. - `templates/jobs/capture_list.html:40-57`: even were the list routed again, a row gets *Review* and *Discard* only when `capture.is_pending`, a link only when `capture.application` is set, and otherwise the bare `{{ capture.get_status_display }}`. A discarded row has nothing to press, and a capture saved as a listing without an application shows its status with no link to `capture.posting`. - `tests/test_table_caption_and_discard.py:30-48` asserts the text of the `href` and never requests it. `tests/test_listings.py:169-170` asserts that `jobs:capture_list` answers 302 to `listings:list`, so the two tests between them pin the broken state. - **What happens:** 1. Somebody triaging presses `d` on the wrong capture, or discards a mistaken selection, and follows *the discarded captures*. 2. `GET /jobs/captures/?show=all` answers 302 to `/listings/`, which shows pending captures and listings. The discarded capture is on neither, and no control on any page lists discarded captures or restores one. 3. The capture can be reached again only by typing `/jobs/captures/<pk>/review/`, which nothing shows, and which works only because the review page does not check the status (#336). Discarding also emptied its `learning` (`jobs/capture_views.py:451`, `:480`), and its kept page is removed `POSTULO_CAPTURE_PAGE_KEEP_DAYS` (30 by default) after it was captured (`jobs/pages.py:828-849`, `expire_unconfirmed`). 4. Separately, with more than 20 captures waiting, Listings shows 20 and its heading says "20 captures waiting for review"; with 25 pending the heading read 20. The rest are reachable only through *next* on a review page. `notifications/slow.py:106` sends the "Captured N postings" notification of a batch to this same address, so a batch of forty lands on a page that says twenty. - **Why it matters:** #260 let the fast discard key stay fast on the strength of the capture being recoverable. It is recoverable only in the database, the message says otherwise, and the test that guards it cannot notice. The count misleads anyone with a real backlog, which is the case #179 was built for. - **Fix:** - Give captures one home that can show what is not pending. Either route `jobs:capture_list` back to `CaptureListView`, or add a discarded-captures view reached from the capture block on Listings (for example `?captures=discarded`) listing `for_user()` captures with status `DISCARDED`. Point `discarded_link()` at it and correct its docstring. `notifications/slow.py:106` should land in the same place. - Add a `jobs:capture_restore` POST view that, under `select_for_update()`, moves a `DISCARDED` capture of the requesting person back to `PENDING`, refuses any other status, and answers 404 for another account's capture. Show a *Put it back* button on each discarded row. Land it with or before #336, so that the review page refusing decided captures does not remove the only way back. - On whichever list survives, link an accepted row to `capture.posting` when it has no application. - Delete whichever of `CaptureListView`, `templates/jobs/capture_list.html` and `capture_list_url` is left unused, and let the catalogues drop the strings. - Count pending captures with a separate `.count()` for the heading, and link to the full list when there are more than 20. ## Done when - `tests/test_table_caption_and_discard.py` discards a capture, follows the link in the message, and asserts that the discarded capture's title and a restore control are on the page it reaches. - A test beside it posts the restore and asserts the capture is pending again (`next_pending` finds it), that restoring a capture that is not discarded is refused, and that another account's capture answers 404. - A test in `tests/test_listings.py` with 25 pending captures asserts the heading says 25 and that the page links to the full list.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:46:16 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#380
No description provided.