A document is a kind, and a kind is a plugin #133

Closed
opened 2026-09-09 10:25:21 +00:00 by tiagoagueda · 1 comment
Owner

Observation

on the documents, general conceived for one kid CV's, shoould in fact extended to other
kinds of documents, once again defined by a deveral internal plugins:

  • CVs
  • Motivation / Cover letters
  • Forfolios (of diferent kinds)
  • Emails
  • Reports (#56)

all of them plaing nice with possible template extentions and other storage plugins like
the future paperless plugin

also multiple language documents cohabiting should be normal

What exists

More than "one kind", and less than five. Authored: CV, which selects from the career
record through CVItem; and CoverLetter, which already carries four shapes of its own —
cover, motivation, speculative, follow-up — told apart by shape rather than by name, with
a note about why lettre de motivation makes the naming a translation hazard.

Held rather than authored: UploadedDocument, whose DocumentKind already lists
PORTFOLIO, CERTIFICATE and REFERENCE — so three of the kinds asked for exist as
files somebody had, and none of them as things Postulo makes.

Frozen: RenderedDocument, the PDF as it went out, with its source text beside it so
"what did I claim?" is answerable without opening anything. Copied: DocumentCopy, one row
per document per store connection, which is the half the future Paperless plugin plugs into
and it is already generic over stores — store, label, external_id, external_url,
retries and a status — and not generic over documents.

What this asks for

Five kinds, each an internal plugin, all playing nicely with themes, stores, and more than
one language at a time.

Worth being careful about

Three of the five are not the same animal, and the differences are structural.

Portfolios are CV-shaped: a selection from the career record with its own template. They
would use CVItem's machinery almost unchanged, and they are where "of different kinds"
bites — a developer's portfolio, a designer's and a researcher's differ in what they select
and how it is laid out, which is either three kinds or one kind with three themes.

Reports (#56) are the first kind whose content is computed, not written. Nothing is
selected and nothing is typed: the document is a query over the record at a moment. That
fits RenderedDocument well — a report handed to an employment office should be frozen
exactly as handed over — but it means "edit this document" has no meaning for the kind, and
the interface has to stop offering it.

Emails cross a boundary the application currently keeps clean. Postulo sends mail through
a transport, and #104 locks the transport when it is the last way back into an account. An
email that is a document is something composed, rendered, frozen and copied to a store; an
email that is a message is something a transport carries. Which of those this kind is
decides whether it belongs in documents at all, and the answer is not obvious: the useful
thing about keeping the email you sent a recruiter is exactly the thing RenderedDocument
already does for a letter.

A kind as a plugin needs a contract, and it is a wide one. A source is handed a URL and
gives back data. A document kind has to declare a model or say it has none, a template per
theme, what a new one starts as (LETTER_STARTERS is the precedent), how it renders to
HTML, how it snapshots to a PDF, and what it is called in the list. That is a much larger
surface than any existing kind, which is why #126 and #129 come first: without a declared
surface and a self-containment rule, five document kinds inside documents/ would be five
more phone-numbers.

Stores already work, and should keep working without knowing about kinds. archiving.py
schedules a copy per document per connection and asks isinstance(document, RenderedDocument). Once the link is polymorphic, a Paperless plugin receives a file, a
title and a kind, and never learns that portfolios exist — which is the right shape and
worth protecting deliberately rather than by accident.

DocumentKind is doing two jobs already. It labels an uploaded file and a rendered
one, and CoverLetter maps its own LetterKind onto it. Five plugin kinds and this
enumeration will disagree about what the list of kinds is. One of them should be derived
from the other.

The staged version is real. Portfolios need the polymorphic link and the theme rule and
nothing else. Reports need those plus a decision about a computed document. Emails need a
boundary drawn with notifications. Doing them in that order means each answer is checked
by something shipping, rather than all of it being designed at once.

## Observation > on the documents, general conceived for one kid CV's, shoould in fact extended to other > kinds of documents, once again defined by a deveral internal plugins: > - CVs > - Motivation / Cover letters > - Forfolios (of diferent kinds) > - Emails > - Reports (#56) > > all of them plaing nice with possible template extentions and other storage plugins like > the future paperless plugin > > also multiple language documents cohabiting should be normal ## What exists More than "one kind", and less than five. Authored: `CV`, which selects from the career record through `CVItem`; and `CoverLetter`, which already carries four shapes of its own — cover, motivation, speculative, follow-up — told apart by *shape* rather than by name, with a note about why *lettre de motivation* makes the naming a translation hazard. Held rather than authored: `UploadedDocument`, whose `DocumentKind` already lists `PORTFOLIO`, `CERTIFICATE` and `REFERENCE` — so three of the kinds asked for exist as *files somebody had*, and none of them as things Postulo makes. Frozen: `RenderedDocument`, the PDF as it went out, with its source text beside it so "what did I claim?" is answerable without opening anything. Copied: `DocumentCopy`, one row per document per store connection, which is the half the future Paperless plugin plugs into and it is already generic over stores — `store`, `label`, `external_id`, `external_url`, retries and a status — and *not* generic over documents. ## What this asks for Five kinds, each an internal plugin, all playing nicely with themes, stores, and more than one language at a time. ## Worth being careful about **Three of the five are not the same animal, and the differences are structural.** *Portfolios* are CV-shaped: a selection from the career record with its own template. They would use `CVItem`'s machinery almost unchanged, and they are where "of different kinds" bites — a developer's portfolio, a designer's and a researcher's differ in what they select and how it is laid out, which is either three kinds or one kind with three themes. *Reports* (#56) are the first kind whose **content is computed, not written**. Nothing is selected and nothing is typed: the document is a query over the record at a moment. That fits `RenderedDocument` well — a report handed to an employment office should be frozen exactly as handed over — but it means "edit this document" has no meaning for the kind, and the interface has to stop offering it. *Emails* cross a boundary the application currently keeps clean. Postulo sends mail through a `transport`, and #104 locks the transport when it is the last way back into an account. An email that is *a document* is something composed, rendered, frozen and copied to a store; an email that is *a message* is something a transport carries. Which of those this kind is decides whether it belongs in `documents` at all, and the answer is not obvious: the useful thing about keeping the email you sent a recruiter is exactly the thing `RenderedDocument` already does for a letter. **A kind as a plugin needs a contract, and it is a wide one.** A source is handed a URL and gives back data. A document kind has to declare a model or say it has none, a template per theme, what a new one starts as (`LETTER_STARTERS` is the precedent), how it renders to HTML, how it snapshots to a PDF, and what it is called in the list. That is a much larger surface than any existing kind, which is why #126 and #129 come first: without a declared surface and a self-containment rule, five document kinds inside `documents/` would be five more `phone-numbers`. **Stores already work, and should keep working without knowing about kinds.** `archiving.py` schedules a copy per document per connection and asks `isinstance(document, RenderedDocument)`. Once the link is polymorphic, a Paperless plugin receives a file, a title and a kind, and never learns that portfolios exist — which is the right shape and worth protecting deliberately rather than by accident. **`DocumentKind` is doing two jobs already.** It labels an uploaded file *and* a rendered one, and `CoverLetter` maps its own `LetterKind` onto it. Five plugin kinds and this enumeration will disagree about what the list of kinds is. One of them should be derived from the other. **The staged version is real.** Portfolios need the polymorphic link and the theme rule and nothing else. Reports need those plus a decision about a computed document. Emails need a boundary drawn with `notifications`. Doing them in that order means each answer is checked by something shipping, rather than all of it being designed at once.
tiagoagueda added this to the 0.3.0 milestone 2026-09-09 10:25:21 +00:00
Author
Owner

Done in 29b85e0a — stage one of the three this issue proposes, with stages two
and three filed as #162. Taking your own advice:

The staged version is real. Portfolios need the polymorphic link and the theme rule and
nothing else. […] Doing them in that order means each answer is checked by something
shipping, rather than all of it being designed at once.

Two of the prerequisites turned out to be already built

Worth saying first, because it changed what was left:

  • The polymorphic link exists. #130 made a render point at whatever made it and a copy
    point at whatever it copied, both generic. The issue text describes two nullable foreign
    keys; that was true when it was written and is not any more.
  • The theme rule exists. #132 gave a theme its own declaration of which kinds it sets,
    and keeps a pair that cannot work out of the menu rather than refusing it after the export
    button.

So what actually remained was the vocabulary, the first kind, and two isinstance calls.

DocumentKind was doing two jobs — one derives from the other now

One of them should be derived from the other.

The values stay in the enumeration, because they are stored, exported and read by the
API, and a value in a database column is not a thing to compute. Everything said about a
kind
— its label, whether Postulo composes one or it only ever arrives as a file, which
theme vocabulary sets it, who provides it — comes from documents/kinds.py.

The two kind columns take their choices from it through a callable, so a kind a plugin
registers reaches every picker and every store's per-kind connection switch with no
migration
. There is a test that registers acme:brief and finds it on the upload form.
That is what makes a kind is a plugin something other than a phrase.

Two tests hold the two vocabularies together in both directions: every storable value is
described, and every described key is either storable or claims a provider.

Portfolios: one model, not two

You put the choice in the issue yourself —

either three kinds or one kind with three themes

— and the answer is one kind, and the themes do the rest. A portfolio is a selection
from the career record with its own layout; it uses every line of CVItem's machinery
unchanged. Two near-identical models would have been two forms, two lists, two exporters and
two of every future change.

The precedent was already here: CoverLetter has carried four shapes told apart by a field
since it arrived. CV now carries two.

What genuinely differs is the order of the argument, which is the one thing a document's
structure is for. So a portfolio gets its own base template rather than a flag inside the
CV one: the work first, each piece with what it points at underneath, and the career
following as a compact context row. Both shipped themes gained a portfolio.html, which
#132's rule requires and a test enforces.

Stores stopped asking what they were holding

Stores already work, and should keep working without knowing about kinds. […] which is the
right shape and worth protecting deliberately rather than by accident.

Deliberately, then. The two remaining isinstance(document, RenderedDocument) calls are
gone; a holder declares download_url_name, archive_origin and archived_at, and
stores.py asks the document. The test describes a class stores.py has never heard of and
gets correct metadata back — so a report, or anything a plugin brings, needs no branch there.
The per-kind switches on a store connection come from the registry too.

What is not here, and why

Reports and emails, as you sequenced them. #162 records what each still needs:

  • A report's hard question was what does "edit this document" mean for computed content —
    and #56 answered it in advance: nothing is stored, a report is a snapshot of the record at
    a moment. What is left is a delivery decision: frozen on Download PDF, or never? Probably
    the former, but it should be decided rather than assumed.
  • Emails are genuinely undecided, exactly as you wrote. #162 adds two things to weigh:
    #104's transport lock, and the possibility that the smallest honest answer is no email
    kind at all
    — a follow-up note is already a LetterKind and is an email in all but
    name, and what is worth keeping is what RenderedDocument already does.

And the wide contract is deliberately not built. A kind declaring a model, a template per
theme, a starter, how it renders and how it snapshots would be a much larger surface than any
other plugin kind here — and most of it already lives somewhere better: a theme declares its
own templates, a LETTER_STARTERS-shaped map declares what a new one starts as, rendering
goes through documents.rendering. Building the rest before anything outside asks for it
would be designing against a guess. The module docstring says so rather than leaving it to be
inferred.

One thing found by accident

A test could be handed a translated string depending on which test ran before it.
LocaleMiddleware activates a language per request and nothing deactivated it, so a test
signing in as somebody reading Postulo in Portuguese left Portuguese active for the rest of
the process. Passing alone, failing in company, failing differently by order — the same shape
as the sign-in limiter and the column-width teardown. Every test starts in the instance's own
language now.

Also

tests/test_document_kinds.py (25). Suite 4770 passed, 29 skipped; browser suite 86 passed.
Eleven new strings in all 39 European catalogues. FORMAT_VERSION is 14; an archive
written before it restores every CV as a CV, which is what every one of them was, and a kind
an archive invents does the same. Wiki: CVs and portfolios.

Shipped on 0.3.0, with main kept level.

Done in `29b85e0a` — **stage one of the three this issue proposes**, with stages two and three filed as **#162**. Taking your own advice: > The staged version is real. Portfolios need the polymorphic link and the theme rule and > nothing else. […] Doing them in that order means each answer is checked by something > shipping, rather than all of it being designed at once. ## Two of the prerequisites turned out to be already built Worth saying first, because it changed what was left: - **The polymorphic link exists.** #130 made a render point at *whatever made it* and a copy point at *whatever it copied*, both generic. The issue text describes two nullable foreign keys; that was true when it was written and is not any more. - **The theme rule exists.** #132 gave a theme its own declaration of which kinds it sets, and keeps a pair that cannot work out of the menu rather than refusing it after the export button. So what actually remained was the vocabulary, the first kind, and two `isinstance` calls. ## `DocumentKind` was doing two jobs — one derives from the other now > One of them should be derived from the other. **The values stay in the enumeration**, because they are stored, exported and read by the API, and a value in a database column is not a thing to compute. **Everything said *about* a kind** — its label, whether Postulo composes one or it only ever arrives as a file, which theme vocabulary sets it, who provides it — comes from `documents/kinds.py`. The two `kind` columns take their choices from it **through a callable**, so a kind a plugin registers reaches every picker and every store's per-kind connection switch with **no migration**. There is a test that registers `acme:brief` and finds it on the upload form. That is what makes *a kind is a plugin* something other than a phrase. Two tests hold the two vocabularies together in both directions: every storable value is described, and every described key is either storable or claims a provider. ## Portfolios: one model, not two You put the choice in the issue yourself — > either three kinds or one kind with three themes — and the answer is **one kind, and the themes do the rest**. A portfolio *is* a selection from the career record with its own layout; it uses every line of `CVItem`'s machinery unchanged. Two near-identical models would have been two forms, two lists, two exporters and two of every future change. The precedent was already here: `CoverLetter` has carried four shapes told apart by a field since it arrived. `CV` now carries two. **What genuinely differs is the order of the argument**, which is the one thing a document's structure is *for*. So a portfolio gets its own base template rather than a flag inside the CV one: the work first, each piece with what it points at underneath, and the career following as a compact context row. Both shipped themes gained a `portfolio.html`, which #132's rule requires and a test enforces. ## Stores stopped asking what they were holding > Stores already work, and should keep working without knowing about kinds. […] which is the > right shape and worth protecting deliberately rather than by accident. Deliberately, then. The two remaining `isinstance(document, RenderedDocument)` calls are gone; a holder declares `download_url_name`, `archive_origin` and `archived_at`, and `stores.py` asks the document. The test describes a class `stores.py` has never heard of and gets correct metadata back — so a report, or anything a plugin brings, needs no branch there. The per-kind switches on a store connection come from the registry too. ## What is not here, and why **Reports** and **emails**, as you sequenced them. #162 records what each still needs: - A report's hard question was *what does "edit this document" mean for computed content* — and #56 answered it in advance: nothing is stored, a report is a snapshot of the record at a moment. What is left is a delivery decision: frozen on *Download PDF*, or never? Probably the former, but it should be decided rather than assumed. - **Emails are genuinely undecided**, exactly as you wrote. #162 adds two things to weigh: #104's transport lock, and the possibility that the smallest honest answer is *no email kind at all* — a *follow-up note* is already a `LetterKind` and is an email in all but name, and what is worth keeping is what `RenderedDocument` already does. **And the wide contract is deliberately not built.** A kind declaring a model, a template per theme, a starter, how it renders and how it snapshots would be a much larger surface than any other plugin kind here — and most of it already lives somewhere better: a theme declares its own templates, a `LETTER_STARTERS`-shaped map declares what a new one starts as, rendering goes through `documents.rendering`. Building the rest before anything outside asks for it would be designing against a guess. The module docstring says so rather than leaving it to be inferred. ## One thing found by accident A test could be handed a **translated** string depending on which test ran before it. `LocaleMiddleware` activates a language per request and nothing deactivated it, so a test signing in as somebody reading Postulo in Portuguese left Portuguese active for the rest of the process. Passing alone, failing in company, failing differently by order — the same shape as the sign-in limiter and the column-width teardown. Every test starts in the instance's own language now. ## Also `tests/test_document_kinds.py` (25). Suite 4770 passed, 29 skipped; browser suite 86 passed. Eleven new strings in all 39 European catalogues. `FORMAT_VERSION` is **14**; an archive written before it restores every CV as a CV, which is what every one of them was, and a kind an archive invents does the same. Wiki: *CVs and portfolios*. Shipped on `0.3.0`, with `main` kept level.
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.

Reference
Postulo/postulo#133
No description provided.