Reference letters: who wrote one, when, and which applications it went with #666

Closed
opened 2026-10-03 01:58:44 +00:00 by tiagoagueda · 1 comment
Owner

From the list of 2026-10-02, under Applications: "Add reference letters." Many applications ask for a letter of reference, and today the best Postulo can do is hold the PDF as an anonymous file. What a person needs to know about one, who wrote it, when, whether it is still good to send and which applications it went with, is not recorded anywhere. This is not the same thing as the career section "References" (the people who will vouch for you), which is #696; the two are kept apart below.

What exists today

  • A kind, and nothing around it. DocumentKind.REFERENCE ("Reference") exists and is registered as a kind that Postulo holds rather than composes (src/postulo/documents/kinds.py:65, :180; authored=False). The Files page and the upload form offer it with the other kinds.
  • An upload has no referee and no date. UploadedDocument carries a title, a kind, the file, notes, a language, a checksum and a version chain (documents/models.py:564-640). Nothing says who wrote the letter, when, or whether it may still be sent. The uploads list has no filter by kind either, though the form promises one (D2, #667).
  • It can be sent with an application. Record what you sent offers every upload under "Files you have already" (documents/forms.py:535-541); the choice is stored on Application.sent_uploads (applications/models.py:411-417) and a "Documents sent" line is written on the timeline (documents/slow.py:117-125). The application's documents page lists them (documents/views.py:880-895).
  • Nothing is composed for a referee. The authored letters are LetterKind cover, motivation, speculative and follow-up (documents/models.py:428-446).
  • A referee has a place to be recorded, but it is not this. A Contact may have no company (jobs/models.py:604-640), and an application records who referred you (Application.referred_by, applications/models.py:377-387, #239): somebody who put your name forward, which is a different act from somebody who wrote about you.
  • No timeline event for it. EventKind has nothing for a reference requested or received (applications/models.py:502-515).
  • Another person's data. A contact's document and an erasure list what the instance holds on one other person (core/gdpr.py:155-181, :277-316). A letter is data about its author, and no row exists today to find it by.
  • Stores copy it by default. Every store connection has a switch per kind, all on unless unset (documents/stores.py:112-125), so a connected Paperless would receive a reference letter the day it is uploaded.

Checked against main at f09b6010b.

Proposal

  1. A record of the letter. A ReferenceLetter side record, one-to-one with an UploadedDocument of kind Reference: the referee (a Contact of the person's own, which may have no company), the date it was written (optional), a date after which it should not be sent (optional), and how it reaches the employer (you send it, or the referee sends it themselves). The file is the existing upload, so storage, checksum, copies and the version chain are unchanged. Deleting the upload deletes the record.
  2. Entered where the file is entered. The upload form shows the referee, the two dates and the delivery when the kind is Reference, with a "new contact" way out for a referee who is not yet a contact. A person with a letter in hand does one step.
  3. Shown where it is chosen and where it went. In Record what you sent, a reference letter is listed with its referee and date, and one past its "do not send after" date says so. On the application's documents page a "Reference letters" group names the referee beside each file. A letter the referee sends themselves is listed as to be sent by them and is not offered as a file to attach.
  4. Shown on the contact. A contact's page lists the letters it wrote and the applications they went with.
  5. The referee's rights. contact_document carries the letters' metadata (referee, dates, file name, applications), and the erasure screen counts them.
  6. A default for stores. See the decision below.
  7. Wiki. Files and what you sent: what a reference letter is, how it is attached and what is copied where.

Decisions

  • What "reference letter" means here. Options: (a) a letter someone wrote about you, which you hold and attach; (b) a request-and-chase workflow with the referee (asked on, due on, received on, sent to the employer); (c) a letter Postulo composes for the referee to sign, as some academic and continental practice has the candidate draft it. Recommended: (a) first. It is the part the code half-has, and it is what the application needs to say it sent. (b) is a separate issue built on this one (events and a reminder per referee), and (c) is a new authored kind with a letter theme. Question for the maintainer: is the note asking for (a), or for (b) or (c) as well?
  • A side record or columns on the upload. Recommended: the side record. A referee is a person, and "valid until" and "who sends it" mean nothing on a CV or a certificate. #669 gives certificates and diplomas what they need (the entry they prove) in its own way.
  • Referee as a contact, as a career entry, or as text. Options: a Contact; an entry of the career's "References" section (#696); free text. Recommended: a Contact, which exists today and which #696 is expected to link too, so the two are joined through the person and neither depends on the other being built first. Free text would leave nothing for an erasure to find.
  • Erasing a referee. The letter is a document the account holder holds, and it holds the referee's words. Options: unlink it and keep it; delete it with the contact; ask. Recommended: the erasure screen lists the letters and the person chooses; the default unlinks and counts them under unlinked (the way ErasureReport already counts applications and listing entries, core/gdpr.py:188-235). Deleting an upload that applications say they sent already warns (documents/views.py:628-650, #217), and that warning applies.
  • Copies to stores. A reference letter is third-party personal data. Options: leave the switch on like every kind; default the Reference switch off on new connections. Recommended: off by default for this kind only, with the reason shown beside the switch. This changes kind_specs from "all on" to "all on but this one" and is recorded here as a deviation from #13's rule that every kind goes.
  • Expiry. A "do not send after" date is the person's, never enforced: it warns, it does not block. Left optional because most letters have none.
  • Distinct from #696. Career "References" lists people who will vouch, with consent and an "available on request" flag, and prints on a CV. A reference letter is a document, held in Documents and sent with an application. They share a Contact and nothing else.

Builds on

#28 and #133 (letter and document kinds, closed), #239 (who referred you, closed), #217 (what was sent stays true, closed).

Needed by

Nothing waits on it. Related: #667 (the Documents page does not link to Files, where this is entered), #663 (an uploaded letter's integrity), #696.

Done when

  • tests/test_documents.py (new tests): an upload of kind Reference takes a referee and dates; another kind ignores and does not show them; deleting the upload deletes the record; deleting the referee unlinks it.
  • tests/test_applications.py and tests/test_document_record.py: a letter attached through Record what you sent shows its referee on the application's documents page; one the referee sends themselves is not offered as an attachment; a past "do not send after" date is shown.
  • tests/test_gdpr.py: the contact's document lists the letters and the erasure screen counts them; unlinking is the default.
  • tests/test_stores.py: a new connection has the Reference switch off, and an existing one keeps the setting it has.
  • New record type, so also: the archive format goes up by one (core/export.py FORMAT_VERSION, 29 on f09b6010b; one more than whatever it is when the commit lands) with an importer test that older archives still load; the candidate format (4) is unchanged (a letter is not part of the career block); the API (api/routers/documents.py) returns the record on an upload; search (core/search.py:390) finds a letter by its referee's name; CV print choice (#308) does not apply; the processing record (core/gdpr.py, drawn from plugins and connections) needs nothing, while contact_document and the erasure do (above); tests/test_wiki_surface.py does not apply (no plugin-surface name); tests/test_page_coverage.py and tests/security/test_isolation_sweep.py for any new page, and a foreign contact is refused as referee; strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349).

From the list of improvements of 2026-10-02.

From the list of 2026-10-02, under Applications: "Add reference letters." Many applications ask for a letter of reference, and today the best Postulo can do is hold the PDF as an anonymous file. What a person needs to know about one, who wrote it, when, whether it is still good to send and which applications it went with, is not recorded anywhere. This is not the same thing as the career section "References" (the people who will vouch for you), which is #696; the two are kept apart below. ## What exists today - **A kind, and nothing around it.** `DocumentKind.REFERENCE` ("Reference") exists and is registered as a kind that Postulo *holds* rather than composes (`src/postulo/documents/kinds.py:65`, `:180`; `authored=False`). The Files page and the upload form offer it with the other kinds. - **An upload has no referee and no date.** `UploadedDocument` carries a title, a kind, the file, notes, a language, a checksum and a version chain (`documents/models.py:564-640`). Nothing says who wrote the letter, when, or whether it may still be sent. The uploads list has no filter by kind either, though the form promises one (D2, #667). - **It can be sent with an application.** *Record what you sent* offers every upload under "Files you have already" (`documents/forms.py:535-541`); the choice is stored on `Application.sent_uploads` (`applications/models.py:411-417`) and a "Documents sent" line is written on the timeline (`documents/slow.py:117-125`). The application's documents page lists them (`documents/views.py:880-895`). - **Nothing is composed for a referee.** The authored letters are `LetterKind` cover, motivation, speculative and follow-up (`documents/models.py:428-446`). - **A referee has a place to be recorded, but it is not this.** A `Contact` may have no company (`jobs/models.py:604-640`), and an application records who *referred* you (`Application.referred_by`, `applications/models.py:377-387`, #239): somebody who put your name forward, which is a different act from somebody who wrote about you. - **No timeline event for it.** `EventKind` has nothing for a reference requested or received (`applications/models.py:502-515`). - **Another person's data.** A contact's document and an erasure list what the instance holds on one other person (`core/gdpr.py:155-181`, `:277-316`). A letter is data about its author, and no row exists today to find it by. - **Stores copy it by default.** Every store connection has a switch per kind, all on unless unset (`documents/stores.py:112-125`), so a connected Paperless would receive a reference letter the day it is uploaded. Checked against `main` at `f09b6010b`. ## Proposal 1. **A record of the letter.** A `ReferenceLetter` side record, one-to-one with an `UploadedDocument` of kind *Reference*: the referee (a `Contact` of the person's own, which may have no company), the date it was written (optional), a date after which it should not be sent (optional), and how it reaches the employer (*you send it*, or *the referee sends it themselves*). The file is the existing upload, so storage, checksum, copies and the version chain are unchanged. Deleting the upload deletes the record. 2. **Entered where the file is entered.** The upload form shows the referee, the two dates and the delivery when the kind is *Reference*, with a "new contact" way out for a referee who is not yet a contact. A person with a letter in hand does one step. 3. **Shown where it is chosen and where it went.** In *Record what you sent*, a reference letter is listed with its referee and date, and one past its "do not send after" date says so. On the application's documents page a "Reference letters" group names the referee beside each file. A letter the referee sends themselves is listed as *to be sent by* them and is not offered as a file to attach. 4. **Shown on the contact.** A contact's page lists the letters it wrote and the applications they went with. 5. **The referee's rights.** `contact_document` carries the letters' metadata (referee, dates, file name, applications), and the erasure screen counts them. 6. **A default for stores.** See the decision below. 7. **Wiki.** *Files and what you sent*: what a reference letter is, how it is attached and what is copied where. ## Decisions - **What "reference letter" means here.** Options: (a) a letter someone wrote about you, which you hold and attach; (b) a request-and-chase workflow with the referee (asked on, due on, received on, sent to the employer); (c) a letter Postulo composes for the referee to sign, as some academic and continental practice has the candidate draft it. **Recommended:** (a) first. It is the part the code half-has, and it is what the application needs to say it sent. (b) is a separate issue built on this one (events and a reminder per referee), and (c) is a new authored kind with a letter theme. Question for the maintainer: is the note asking for (a), or for (b) or (c) as well? - **A side record or columns on the upload.** **Recommended:** the side record. A referee is a person, and "valid until" and "who sends it" mean nothing on a CV or a certificate. #669 gives certificates and diplomas what they need (the entry they prove) in its own way. - **Referee as a contact, as a career entry, or as text.** Options: a `Contact`; an entry of the career's "References" section (#696); free text. **Recommended:** a `Contact`, which exists today and which #696 is expected to link too, so the two are joined through the person and neither depends on the other being built first. Free text would leave nothing for an erasure to find. - **Erasing a referee.** The letter is a document the account holder holds, and it holds the referee's words. Options: unlink it and keep it; delete it with the contact; ask. **Recommended:** the erasure screen lists the letters and the person chooses; the default unlinks and counts them under `unlinked` (the way `ErasureReport` already counts applications and listing entries, `core/gdpr.py:188-235`). Deleting an upload that applications say they sent already warns (`documents/views.py:628-650`, #217), and that warning applies. - **Copies to stores.** A reference letter is third-party personal data. Options: leave the switch on like every kind; default the *Reference* switch off on new connections. **Recommended:** off by default for this kind only, with the reason shown beside the switch. This changes `kind_specs` from "all on" to "all on but this one" and is recorded here as a deviation from #13's rule that every kind goes. - **Expiry.** A "do not send after" date is the person's, never enforced: it warns, it does not block. Left optional because most letters have none. - **Distinct from #696.** Career "References" lists *people* who will vouch, with consent and an "available on request" flag, and prints on a CV. A reference *letter* is a *document*, held in Documents and sent with an application. They share a `Contact` and nothing else. ## Builds on #28 and #133 (letter and document kinds, closed), #239 (who referred you, closed), #217 (what was sent stays true, closed). ## Needed by Nothing waits on it. Related: #667 (the Documents page does not link to Files, where this is entered), #663 (an uploaded letter's integrity), #696. ## Done when - `tests/test_documents.py` (new tests): an upload of kind *Reference* takes a referee and dates; another kind ignores and does not show them; deleting the upload deletes the record; deleting the referee unlinks it. - `tests/test_applications.py` and `tests/test_document_record.py`: a letter attached through *Record what you sent* shows its referee on the application's documents page; one the referee sends themselves is not offered as an attachment; a past "do not send after" date is shown. - `tests/test_gdpr.py`: the contact's document lists the letters and the erasure screen counts them; unlinking is the default. - `tests/test_stores.py`: a new connection has the *Reference* switch off, and an existing one keeps the setting it has. - New record type, so also: the archive format goes up by one (`core/export.py` `FORMAT_VERSION`, 29 on `f09b6010b`; one more than whatever it is when the commit lands) with an importer test that older archives still load; the candidate format (4) is unchanged (a letter is not part of the career block); the API (`api/routers/documents.py`) returns the record on an upload; search (`core/search.py:390`) finds a letter by its referee's name; CV print choice (#308) does not apply; the processing record (`core/gdpr.py`, drawn from plugins and connections) needs nothing, while `contact_document` and the erasure do (above); `tests/test_wiki_surface.py` does not apply (no plugin-surface name); `tests/test_page_coverage.py` and `tests/security/test_isolation_sweep.py` for any new page, and a foreign contact is refused as referee; strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349). From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.6.0 milestone 2026-10-03 01:58:44 +00:00
Author
Owner

Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#666).

Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#666).
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.

Dependencies

No dependencies set

Reference
Postulo/postulo#666
No description provided.