Keep a shared contact's deletion to the deleter's own rows (#956, #961) #994

Merged
tiagoagueda merged 1 commit from sec2-a2-contact-deletion into main 2026-10-18 08:38:46 +00:00
Owner

#956 (High): "Delete the letters they wrote as well" deleted other members' uploads

ContactDeleteView read reference_letters through the contact with no owner filter, both to count and to delete. On a shared contact each member may hold their own letter from the referee, so the owner's tick deleted every member's UploadedDocument. Both reads now go through one own_letters() (the requester's rows), so the dialog count and the deletion are the deleter's only.

#961 (Medium): data-protection branch skipped the copy others are promised

The gdpr.is_offered branch returned before KeepsOthersCopies.form_valid, so detach.after_losing never ran and erase_contact cascaded other owners' career Reference rows and unlinked their applications, appointments and letters. The copy step is now KeepsOthersCopies.keep_others_copies() and runs first in the erasure branch, inside the same savepoint (a refused erasure rolls the copies back with the rest). erase_contact(contact, reader=None) counts the reader's rows only in the summary it shows.

Tests

tests/security/test_sharing_sweep.py: a shared referee with letters from two members, the grantee also holding an application naming the contact and a career Reference; read and read/write grants, data-protection module on and off. They check that the grantee's upload survives, that the dialog counts one letter, that the summary mentions no application or letter of the grantee, and that the grantee's Reference, application and letter point at a copy they own. Without the fix 5 of the 6 cases fail (the copy case with the module off passed before, as that path was already right).

Deviation: Reference.save() refuses a referee that is not the owner's own, so the test builds the grantee's Reference and then repoints it with QuerySet.update, the state a grantee is left in; the model check is unchanged.

Full unit suite: 18845 passed; the 25 failures of the first run were all translation/language tests (stale uncompiled catalogues in the fresh worktree), and the five files they sit in pass after messages.py compile (384 passed). The browser suite was not run; no template or JS was touched.

Wiki: Files-and-what-you-sent.md commit 4736b81 (not pushed).

Closes #956, Closes #961

🤖 Generated with Claude Code

## #956 (High): "Delete the letters they wrote as well" deleted other members' uploads `ContactDeleteView` read `reference_letters` through the contact with no owner filter, both to count and to delete. On a shared contact each member may hold their own letter from the referee, so the owner's tick deleted every member's `UploadedDocument`. Both reads now go through one `own_letters()` (the requester's rows), so the dialog count and the deletion are the deleter's only. ## #961 (Medium): data-protection branch skipped the copy others are promised The `gdpr.is_offered` branch returned before `KeepsOthersCopies.form_valid`, so `detach.after_losing` never ran and `erase_contact` cascaded other owners' career `Reference` rows and unlinked their applications, appointments and letters. The copy step is now `KeepsOthersCopies.keep_others_copies()` and runs first in the erasure branch, inside the same savepoint (a refused erasure rolls the copies back with the rest). `erase_contact(contact, reader=None)` counts the reader's rows only in the summary it shows. ## Tests `tests/security/test_sharing_sweep.py`: a shared referee with letters from two members, the grantee also holding an application naming the contact and a career `Reference`; read and read/write grants, data-protection module on and off. They check that the grantee's upload survives, that the dialog counts one letter, that the summary mentions no application or letter of the grantee, and that the grantee's `Reference`, application and letter point at a copy they own. Without the fix 5 of the 6 cases fail (the copy case with the module off passed before, as that path was already right). Deviation: `Reference.save()` refuses a referee that is not the owner's own, so the test builds the grantee's `Reference` and then repoints it with `QuerySet.update`, the state a grantee is left in; the model check is unchanged. Full unit suite: 18845 passed; the 25 failures of the first run were all translation/language tests (stale uncompiled catalogues in the fresh worktree), and the five files they sit in pass after `messages.py compile` (384 passed). The browser suite was not run; no template or JS was touched. Wiki: `Files-and-what-you-sent.md` commit 4736b81 (not pushed). Closes #956, Closes #961 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Keep a shared contact's deletion to the deleter's own rows
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 34m4s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m13s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m41s
CI / Browser tests (Chromium) (pull_request) Failing after 48m33s
CI / Security audit (pull_request) Successful in 1m57s
c86767d402
Since #896 a contact can be shared, and a reference letter stays person-owned, so two
members may each hold one from the same referee. ContactDeleteView read the letters through
the contact's reverse relation with no owner filter, both to count them and to delete them,
so "Delete the letters they wrote as well" deleted the uploads of every member who recorded
a letter from that referee. It now reads the requester's own letters only.

With the data-protection module on, the view returned before KeepsOthersCopies.form_valid, so
detach.after_losing never ran and erase_contact cascaded every owner's career Reference rows
and unlinked the others' applications, appointments and letters with no copy. The copy step
is now its own method and runs first in the erasure branch too, inside the same savepoint, so
a refused erasure rolls it back. erase_contact takes the person who asked and counts only
their rows in the summary.

Tests share a referee with letters from two members and run the plain and the
data-protection branch, read and read/write grants.

Closes #956
Closes #961

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed sec2-a2-contact-deletion from c86767d402
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 34m4s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m13s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m41s
CI / Browser tests (Chromium) (pull_request) Failing after 48m33s
CI / Security audit (pull_request) Successful in 1m57s
to 7ea2fec72f
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m43s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m16s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m21s
CI / Security audit (pull_request) Successful in 45s
CI / Browser tests (Chromium) (pull_request) Successful in 28m55s
2026-10-18 07:32:18 +00:00
Compare
Sign in to join this conversation.
No reviewers
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!994
No description provided.