Sync state for plugins, a listing's skills, the monthly report, and a page per contact #1239

Merged
tiagoagueda merged 8 commits from f-stack2 into main 2026-11-12 10:09:21 +00:00
Owner

Four 0.6.0 changes stacked on one branch so they share one CI run; each keeps its own commits, and the catalogues are extracted once at the end. Merge fast-forward-only.

  • Closes #1165: a sync plugin keeps a small state per connection between runs (sync_state, SyncReport(state=…)), written only after a run that did not fail, dropped when the connection's settings or secrets change, at most 8 kB of JSON; migration plugins 0014_connection_sync_state. Accepted by the maintainer as built.
  • Closes #1093: a listing's page and its application's page show which of the ESCO skills the listing names your career record has, and the CV that prints the most of them; matched on ESCO's preferred names with no model; two queries more on the listing page; when ESCO is not downloaded, a note says how to fetch it.
  • Closes #1095: last month's report made on a chosen day, or some days before a plugin's calendar entry; filed under Sent documents with a reminder; handed to stores or emailed from the person's own outbox only if switched on; claimed once per month per person, looked at hourly; migration accounts 0053_report_schedule, archive format 83.
  • Closes #1097: a page for each contact, with how to reach them and everything recorded with them, newest first, at a fixed number of queries (27 at 6 and at 30 entries); a contact's name links to it only where the reader may open it.

Tested locally on the stack: lint, migrations, catalogues, and the full unit suite: 16,575 passed. The one failure is tests/test_wiki_surface.py (CI skips it): the wiki's Writing-a-plugin page gains #1165's names after the merge.

For review: #1093 reads a listing in whichever of the person's languages finds the most skills (a listing has no language field) and matches preferred names only; #1095's "last month" is the month before the day the report is made; #1097 lists only rows that point at the contact directly (an application's own emails and calls have no contact field).

🤖 Generated with Claude Code

Four 0.6.0 changes stacked on one branch so they share one CI run; each keeps its own commits, and the catalogues are extracted once at the end. Merge fast-forward-only. - Closes #1165: a sync plugin keeps a small state per connection between runs (`sync_state`, `SyncReport(state=…)`), written only after a run that did not fail, dropped when the connection's settings or secrets change, at most 8 kB of JSON; migration `plugins 0014_connection_sync_state`. Accepted by the maintainer as built. - Closes #1093: a listing's page and its application's page show which of the ESCO skills the listing names your career record has, and the CV that prints the most of them; matched on ESCO's preferred names with no model; two queries more on the listing page; when ESCO is not downloaded, a note says how to fetch it. - Closes #1095: last month's report made on a chosen day, or some days before a plugin's calendar entry; filed under Sent documents with a reminder; handed to stores or emailed from the person's own outbox only if switched on; claimed once per month per person, looked at hourly; migration `accounts 0053_report_schedule`, archive format 83. - Closes #1097: a page for each contact, with how to reach them and everything recorded with them, newest first, at a fixed number of queries (27 at 6 and at 30 entries); a contact's name links to it only where the reader may open it. Tested locally on the stack: lint, migrations, catalogues, and the full unit suite: 16,575 passed. The one failure is `tests/test_wiki_surface.py` (CI skips it): the wiki's Writing-a-plugin page gains #1165's names after the merge. For review: #1093 reads a listing in whichever of the person's languages finds the most skills (a listing has no language field) and matches preferred names only; #1095's "last month" is the month before the day the report is made; #1097 lists only rows that point at the contact directly (an application's own emails and calls have no contact field). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A sync plugin had nowhere to note how far it got, so postulo-imap, in the
modes that leave the mailbox untouched, downloaded every unrecognised
message of its look-back again on every pass. A run now reads its last
state with `sync_state(connection)` and returns the next one as
`SyncReport.state`; both are on the plugin surface, with the bound
`MAX_SYNC_STATE_BYTES` (8 kB), and `sync` takes no new argument, so a
plugin pinned to an older core runs unchanged.

The state lives in a new `Connection.sync_state` JSON column and is kept
only after a run that did not fail, in the transaction that records the
report: a run that raised, reported an error, ended with
`ConnectionUnusable` or lost its lease writes nothing. `None` leaves what
is kept, an empty dict forgets it. The connection's own save drops it when
the settings or secrets change (not for a report, the switch, the name,
the interval, renewed consent tokens or what *Look on the server* found),
and agreeing again at a provider drops it too. A state too large or not
JSON fails that run with a sentence on the connection and keeps the last
good one, so a broken plugin is seen rather than re-reading for ever.
The column is in no form, page, export or API answer, and goes with its
connection. `postulo.plugins.testing` gains `run_sync` and
`kept_sync_state` for plugin suites.

Closes #1165

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main moved under this branch, and the catalogues are written from the
code as a whole, so they are extracted again rather than merged by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Somebody reading a listing asks which of its requirements they meet.
Postulo held both halves of the answer -- the ESCO skill each of the
person's skills is (#266) and the classification -- and compared
neither.

A listing's page and its application's page now draw a Skills card:
the ESCO skills the listing's title and text name, in the order they
are named, split into those the career record has and those it does
not, and the CV that prints the most of the ones held. No model and
nothing fetched: a skill is named where a run of the text's words is
its preferred name, folded as esco.skill_for folds a typed one, the
longest run winning, and the bracketed sense ESCO adds left off
("Python" names "Python (computer programming)"). A name two skills
share names neither. The person has a skill where one of theirs keeps
the same identifier.

The text is read in the career record's language, the reader's and
English, and the language that finds the most is the answer, since a
listing carries no language of its own. Each is looked up by halving
the sorted order the skill box already holds, so nothing is built
beside what #1110 keeps: one page reads at most those three languages.
What a text names is remembered per process by a digest of the text
and the revision, so an edited description is a new key, and forget()
drops it. On the real v1.2.1 file a 600-word listing takes 300 ms the
first time in a process (reading English), 74 ms for a new text after,
and 1 ms when drawn again; 2.4 MB held, as any English skill lookup.

The page costs two queries more, what the person has and the best CV,
and none where the skills have not been downloaded: there the card
says an administrator runs manage.py fetch_esco, rather than that the
listing names none.

Closes #1093

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The period report is what a person hands an employment office as evidence of
their search, and they had to make it by hand each month. A setting at the foot
of the report page now has the scheduler's pass make last month's report on a
day of each month, or some days before an entry a plugin placed on the calendar
(such as a monthly declaration). The report is drawn by the errand the page's
button already sends, filed under Sent documents, and a reminder to read it is
put on the calendar, which the next pass announces through the notifiers.

It is prepared by default: a report is evidence a person should read first, so
the one the scheduler makes is filed without a copy queued for any store
(`snapshot_report(kept_back=True)`), and Send now on the document hands it on
once read. A second switch, off by default, hands it on at once: to the stores
that take reports, and through the person's own outbox to an address they typed.
A report that could not be made, or not emailed, says so on the reminder.

The pass looks at most once an hour behind a key in the cache, as the media
sweep does, and claims each month per person by comparing the day it was last
made for, as quiet applications are claimed, so two schedulers make it once.

The settings are on the profile, so they travel in the archive (format 83),
read back against their columns, and go with the account.

Closes #1095

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A contact had a form and nothing else, so before calling a recruiter there
was no one place to see which applications they were on, which interviews
they were at, what they sent about a listing and which letters they wrote.

The new page at /jobs/contacts/<id>/ shows the details, the company and
department, the reader's own note, and one history, newest first, of every
row that names the contact by a foreign key: applications where they are
the main contact or who referred the person, interviews, appointments,
entries in a listing's history and reference letters, each linking to its
application, listing or file; and a line when they are one of the career
record's references. It is read-only. Every row is the reader's own
(for_user), so a shared contact shows each reader only their own history,
and the page costs the same number of queries however long that history is.

The contact's address is now this page: the company page, the application
page (main contact and referrer), the site search and the map link to it,
and the edit form's Cancel goes back to it.

Closes #1097

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An application can name somebody whose contact the reader can no longer
see, a contact that was shared and then was not; their name linked to a
page that answered 404. The application page now asks once which of the
two people it names (main contact and referrer) the reader may open, by
the rule the contact's page uses itself, and draws the others as a name.

Refs #1097

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record #1093, #1095 and #1097 in the changelog and extract their strings
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 16m42s
CI / Security audit (pull_request) Successful in 29s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m54s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m44s
CI / Browser tests (Chromium) (pull_request) Successful in 39m12s
CI / Unit tests and coverage (Python 3.14) (push) Successful in 14m52s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m37s
CI / Database tests on PostgreSQL (push) Successful in 4m23s
CI / Security audit (push) Successful in 20s
README / README language tables (push) Successful in 20s
CI / Browser tests (Chromium) (push) Successful in 37m23s
Dev image / Dev image: build, scan, push (push) Failing after 1h8m26s
009deb43f5
The three features were stacked here with #1165 so they share one CI run.
Their catalogues are extracted once, from the code as a whole, rather
than merged from each branch's own extraction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiagoagueda deleted branch f-stack2 2026-11-12 10:09:24 +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!1239
No description provided.