Give SyncLink a collection, a slot, unbind and a lookup by UID (#326) #1079

Merged
tiagoagueda merged 1 commit from 326-synclink-slots into main 2026-10-29 18:25:40 +00:00
Owner

The DAV plugin's next improvements need a link to say three things it could
not: which collection a twin lives in, which of a record's several twins it
is, and that a person undid a pairing. A plugin cannot own tables (#228), so
they belong on SyncLink.

  • collection is the address of the address book, calendar or task list
    holding the twin. A UID is unique only within its collection (RFC 6352
    §5.1, RFC 4791 §4.1), so a twin is keyed on both and remote_href is a
    cache.
  • slot is free text a plugin chooses; blank is the only twin, which every
    existing row is. One link per (connection, record, slot) replaces one per
    (connection, record). for_record and bind take slot="", of_model
    returns every slot.
  • (connection, collection, uid) is unique among paired links where both are
    set, so one remote item is paired with one record. bind raises
    AlreadyPaired, carrying the link that holds the item, instead of a bare
    IntegrityError. It is on the plugin surface.
  • unbind keeps the row and sets unpaired_at, the maintainer's option (b):
    a plugin pairing by UID would otherwise pair the two again on its next run.
    An unpaired row does not count towards the rule above. bind pairs again,
    clearing unpaired_at and, unless passed, remote_gone.
  • for_remote(connection, collection, uid) finds the paired link, else the
    one most recently unpaired.

The migration adds the three columns blank, so it keeps every link, including
two holding one UID on a re-added connection (postulo/postulo-dav#7), which
the new rule does not reach while their collection is blank. Pairings stay
out of the personal export. No admin registration: the admin is off unless
POSTULO_ADMIN_URL is set (#116) and holds nobody's records (#368).

Closes #326

Notes for review

  • SyncLink gains three fields:
    • slot, so one record can have several twins: unique on (connection, record, slot);
    • collection, so one item belongs to one record: unique on (connection, collection, uid) for paired rows;
    • unpaired_at, plus a paired property.
  • New calls:
    • for_remote;
    • bind(slot=…), which raises the new surface name AlreadyPaired instead of a database error;
    • unbind, which keeps the row and stamps unpaired_at, as the issue recommends.
  • Migration: plugins 0013_synclink_slots. Every existing link is kept.
  • Not built: the staff view (point 6). The admin is off unless POSTULO_ADMIN_URL is set (#116), and it holds nobody's records by rule (#368, test_admin_exposure).
  • Follow-up: postulo-dav must check link.paired before #327 can unpair links, filed there.

🤖 Generated with Claude Code

The DAV plugin's next improvements need a link to say three things it could not: which collection a twin lives in, which of a record's several twins it is, and that a person undid a pairing. A plugin cannot own tables (#228), so they belong on SyncLink. - `collection` is the address of the address book, calendar or task list holding the twin. A UID is unique only within its collection (RFC 6352 §5.1, RFC 4791 §4.1), so a twin is keyed on both and `remote_href` is a cache. - `slot` is free text a plugin chooses; blank is the only twin, which every existing row is. One link per (connection, record, slot) replaces one per (connection, record). `for_record` and `bind` take `slot=""`, `of_model` returns every slot. - (connection, collection, uid) is unique among paired links where both are set, so one remote item is paired with one record. `bind` raises `AlreadyPaired`, carrying the link that holds the item, instead of a bare IntegrityError. It is on the plugin surface. - `unbind` keeps the row and sets `unpaired_at`, the maintainer's option (b): a plugin pairing by UID would otherwise pair the two again on its next run. An unpaired row does not count towards the rule above. `bind` pairs again, clearing `unpaired_at` and, unless passed, `remote_gone`. - `for_remote(connection, collection, uid)` finds the paired link, else the one most recently unpaired. The migration adds the three columns blank, so it keeps every link, including two holding one UID on a re-added connection (postulo/postulo-dav#7), which the new rule does not reach while their collection is blank. Pairings stay out of the personal export. No admin registration: the admin is off unless POSTULO_ADMIN_URL is set (#116) and holds nobody's records (#368). Closes #326 ## Notes for review - **`SyncLink` gains three fields:** - `slot`, so one record can have several twins: unique on (connection, record, slot); - `collection`, so one item belongs to one record: unique on (connection, collection, uid) for paired rows; - `unpaired_at`, plus a `paired` property. - **New calls:** - `for_remote`; - `bind(slot=…)`, which raises the new surface name `AlreadyPaired` instead of a database error; - `unbind`, which keeps the row and stamps `unpaired_at`, as the issue recommends. - **Migration:** `plugins 0013_synclink_slots`. Every existing link is kept. - **Not built:** the staff view (point 6). The admin is off unless `POSTULO_ADMIN_URL` is set (#116), and it holds nobody's records by rule (#368, `test_admin_exposure`). - **Follow-up:** postulo-dav must check `link.paired` before #327 can unpair links, filed there. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Give SyncLink a collection, a slot, unbind and a lookup by UID (#326)
Some checks failed
CI / Unit tests and coverage (Python 3.14) (push) Successful in 14m21s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 4m55s
CI / Database tests on PostgreSQL (push) Successful in 7m18s
CI / Security audit (push) Successful in 3m8s
CI / Browser tests (Chromium) (push) Successful in 41m2s
Every Python / Unit tests (Python 3.12) (push) Successful in 15m29s
Every Python / Unit tests (Python 3.13) (push) Successful in 18m30s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 14m17s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 5m13s
CI / Database tests on PostgreSQL (pull_request) Successful in 5m56s
CI / Security audit (pull_request) Successful in 1m17s
CI / Browser tests (Chromium) (pull_request) Failing after 34m13s
c7ee4ec421
The DAV plugin's next improvements need a link to say three things it could
not: which collection a twin lives in, which of a record's several twins it
is, and that a person undid a pairing. A plugin cannot own tables (#228), so
they belong on SyncLink.

- `collection` is the address of the address book, calendar or task list
  holding the twin. A UID is unique only within its collection (RFC 6352
  §5.1, RFC 4791 §4.1), so a twin is keyed on both and `remote_href` is a
  cache.
- `slot` is free text a plugin chooses; blank is the only twin, which every
  existing row is. One link per (connection, record, slot) replaces one per
  (connection, record). `for_record` and `bind` take `slot=""`, `of_model`
  returns every slot.
- (connection, collection, uid) is unique among paired links where both are
  set, so one remote item is paired with one record. `bind` raises
  `AlreadyPaired`, carrying the link that holds the item, instead of a bare
  IntegrityError. It is on the plugin surface.
- `unbind` keeps the row and sets `unpaired_at`, the maintainer's option (b):
  a plugin pairing by UID would otherwise pair the two again on its next run.
  An unpaired row does not count towards the rule above. `bind` pairs again,
  clearing `unpaired_at` and, unless passed, `remote_gone`.
- `for_remote(connection, collection, uid)` finds the paired link, else the
  one most recently unpaired.

The migration adds the three columns blank, so it keeps every link, including
two holding one UID on a re-added connection (postulo/postulo-dav#7), which
the new rule does not reach while their collection is blank. Pairings stay
out of the personal export. No admin registration: the admin is off unless
POSTULO_ADMIN_URL is set (#116) and holds nobody's records (#368).

Closes #326

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed 326-synclink-slots from c7ee4ec421
Some checks failed
CI / Unit tests and coverage (Python 3.14) (push) Successful in 14m21s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 4m55s
CI / Database tests on PostgreSQL (push) Successful in 7m18s
CI / Security audit (push) Successful in 3m8s
CI / Browser tests (Chromium) (push) Successful in 41m2s
Every Python / Unit tests (Python 3.12) (push) Successful in 15m29s
Every Python / Unit tests (Python 3.13) (push) Successful in 18m30s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 14m17s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 5m13s
CI / Database tests on PostgreSQL (pull_request) Successful in 5m56s
CI / Security audit (pull_request) Successful in 1m17s
CI / Browser tests (Chromium) (pull_request) Failing after 34m13s
to c3355db385
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 12m56s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m44s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m58s
CI / Security audit (pull_request) Successful in 1m7s
CI / Browser tests (Chromium) (pull_request) Successful in 33m53s
2026-10-29 10:27:16 +00:00
Compare
tiagoagueda force-pushed 326-synclink-slots from c3355db385
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 12m56s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m44s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m58s
CI / Security audit (pull_request) Successful in 1m7s
CI / Browser tests (Chromium) (pull_request) Successful in 33m53s
to 4ea7edd5f9
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m12s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m43s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m43s
CI / Security audit (pull_request) Successful in 54s
CI / Browser tests (Chromium) (pull_request) Successful in 34m0s
2026-10-29 18:22:47 +00:00
Compare
tiagoagueda deleted branch 326-synclink-slots 2026-10-29 18:25:43 +00:00
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!1079
No description provided.