A kept page follows its capture, then the listing it became, and goes when that listing goes #664
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#664
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The maintainer's rule, from the list of 2026-10-02: "When keep capture source and/or rendering is set, such data is stored on the database. A capture deletion, delete the corresponding stored data. Unless capture translated to confirmed listing, in such case, data is migrated to database listing entry. Only if the listing is deleted the data is permanently deleted." Today the data is kept as files, and for a confirmed capture the code does the opposite of the last sentence: deleting the listing leaves the capture and its page behind for good.
What exists today
CapturedPage(src/postulo/jobs/models.py:1212-1294) is one row per capture (OneToOneField,on_delete=CASCADE,:1235-1240) holding two optional files: the source, gzipped text capped at 2,000,000 bytes (POSTULO_CAPTURE_SOURCE_MAX_BYTES,config/settings/base.py:594), and a rendering (PNG, JPEG, WebP or PDF) capped at 10 MiB (:595-597). An account's kept pages may weigh 1 GiB together (:602-604). Both switches are off by default (:587-588). They are files undercaptures/<owner>/<YYYY>/<MM>/(page_upload_to,:1178-1180), not rows. Where the maintainer's rule says "database", this is the same question as #662.jobs/capture_views.py:326-380,:383-440,:443-460). Discard setsstatus = DISCARDEDand nothing else (:449-452); the bulk version is anUPDATE(:475-480). Nothing in the interface deletes aCapture. The only ways a row goes are deleting the account and the admin (jobs/admin.py:57). The list of discarded captures that #260 promised does not exist (#380, open).CapturedPageForgetView(jobs/page_views.py:174-200,jobs/urls.py:90-92, linked fromtemplates/jobs/capture_page.html:114, :181, :191) asks, then deletes the source, the rendering or both of one capture, for any capture, a confirmed one included; “the capture itself stays” (pages.forget). So the person can already delete the stored data by hand; what they cannot do is delete the capture, or have the data follow the listing.pages.expire_unconfirmeddeletes the page of every capture that is notACCEPTEDonce the capture is older thanPOSTULO_CAPTURE_PAGE_KEEP_DAYS(30 by default; 0 keeps all), run by the scheduler (jobs/pages.py:800-823,notifications/management/commands/send_due_reminders.py:203-205). The capture row stays. Its files go through thepost_deletereceiver (jobs/signals.py:57-84, after commit and only where no other row uses the name), which also runs for a cascade, so deleting aCapturerow takes its files (tests/test_captured_pages.py:880).status = ACCEPTEDandposting = listing(jobs/capture_views.py:351-352); binding does the same (jobs/history.py:239-241).bind_capture's docstring says the capture "is kept and pointed at, not copied: what it read, the address it was read at, and whatever it kept of the page (#256) stay on the capture, which becomes the listing's" (jobs/history.py:206-219, #270). The listing reaches the page through the capture:capture__posting=posting(jobs/views.py:865-869, shown as "The page as captured" intemplates/jobs/posting_detail.html:151-163). An accepted capture is excluded from expiry, "for as long as it exists" (pages.py:800-807, the wiki's Capturing postings: "A capture you saved keeps its page").Capture.postingisSET_NULL(jobs/models.py:1110-1118).PostingDeleteViewsays so before it happens: "The %(count)d file or capture its history points at is kept where it is." (jobs/views.py:917,:972-982, #270). The capture is thenACCEPTEDwith no listing, which the expiry skips, which the interface lists nowhere, and which nothing but the account's deletion removes. Its page goes on counting against the account's 1 GiB, and the "Settings → Capture" page's total.learningon the capture), #524 (expiry loses what the review would learn), #336 (the review page renders a decided capture), #355 (account deletion's hand-kept lists).Checked against
mainatf09b6010b.Proposal
Capture.postinggoes fromSET_NULLtoCASCADE(one migration, no data change). The existingpost_deletereceiver removes the files after the commit; the cascade runs the collector, which sends the signal for every row (jobs/signals.py:1-15).bind_capture's captures follow, because they set the same field. No new ownership model, no archive change:captures[].posting_idandcaptures[].pagealready say it.PostingDeleteViewcounts the captures and their kept pages among the consequences ("%(count)d kept page of a capture") and no longer says a capture "is kept where it is"; the sentence stays true for an uploaded file, which is still not deleted with a listing.POSTto a newjobs:capture_deletefor a pending or discarded capture removes the row and, through the receiver, its page, at once. Offered on the review page and on the list of discarded captures that #380 restores; it is refused for anACCEPTEDcapture, whose data is the listing's.manage.py prune_captures(listing by default,--removeto act, asprune_mediadoes) names everyACCEPTEDcapture that has no listing and no application, with its page and what it weighs. Not a migration: the files are somebody's, and the rule that applies to those rows was not the rule when they were made.POSTULO_CAPTURE_PAGE_KEEP_DAYS: "A capture somebody saved keeps its page" becomes "keeps its page while the listing exists"), and the docstrings ofjobs/pages.py,jobs/history.pyandCapturedPage.Decisions
CASCADE); (B) the page stays on the capture and apre_deletereceiver on the listing deletes just the pages. Recommended: A'. It makes the rule true with one migration and no new URLs (the page routes are keyed by capture id,jobs/urls.py:70-92), keeps #270's design, and leaves the archive untouched. B leaves a decided capture with no listing and no page, a row that means nothing. A is the literal reading of "migrated to the listing" and is the choice if decided captures are ever to be purged while their pages stay; it needs the page routes re-addressed, an archive bump (29) and the importer's_restore_kept_pagemoved. A' does not stop A being done later.JobPosting). The other candidate is an entry in the listing's history (ListingEvent, #270), which a bound capture already has and which points at the capture. Under A' the page is reachable from both, and goes with both.POSTULO_CAPTURE_PAGE_KEEP_DAYS).PostingDeleteViewtells the person a capture survives its listing. This changes that, for pages and captures, and not for uploaded files. Record in the changelog under Changed.prune_mediahas: "the failure mode of the opposite default is somebody's CV".Builds on
#256 (closed), #270 (closed), #260 (closed), #217 (closed), #380, #523, #524, #336, #355.
Needed by
Part 3 needs a list of discarded captures, which is #380; part 3 may ship with a minimal one if #380 has not landed.
Done when
tests/test_captured_pages.py: deleting a listing that a capture became removes the capture, the page row and both files after commit (besidetest_deleting_a_capture_takes_its_files; fails today); a bound capture's page goes with the listing it was bound to; a rolled-back listing deletion keeps them; a pending or discarded capture's page is untouched by deleting somebody else's listing; deleting a pending capture throughjobs:capture_deleteremoves its files, and the same request on anACCEPTEDcapture is refused.tests/test_listing_history.py: the history entries that pointed at the deleted capture go with the listing and nothing dangles.tests/test_listing_history.py:493pins today's sentence ("The 1 file or capture its history points at is kept where it is."); it changes with part 2, and a new test there asserts that the confirmation page counts the kept pages.prune_captureslists and removes a stranded page and leaves a live one (tests/test_captured_pages.py).tests/security/test_isolation_sweep.pyandtests/test_page_coverage.pyforjobs:capture_delete(another person's capture is a 404; a sign-in is needed).FORMAT_VERSION(29 onf09b6010b,core/export.py:80) is unchanged under A'; under A it goes up by one (one more than whatever it is when the commit lands). API:GET /capturesis unchanged (pending only); a capture of a deleted listing is a 404. Processing record (core/gdpr.py) unchanged: nothing new is kept, less is kept for less time. Strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349); the wiki pages in part 5.From the list of improvements of 2026-10-02.
Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#664).