More kinds of documents: motivation letters, portfolios as links, video CVs #28

Closed
opened 2026-09-05 13:52:33 +00:00 by tiagoagueda · 0 comments
Owner

Observation

on the documents, we should introduce other kinds of documents like motivation letters / portfolios / video cvs

What exists today

  • Three shapes of document. A CV, structured and rendered. A cover letter (CoverLetter: name, subject, body, template flag, theme) — one kind of letter, with no field to say which kind. An uploaded document with a DocumentKind of CV, cover letter, certificate, portfolio, reference or other (documents/models.py, line 190). So "portfolio" exists already — as a file.
  • Uploads accept pdf doc docx odt rtf txt png jpg jpeg, 20 MB per file (MAX_UPLOAD_BYTES, documents/forms.py; the wiki says so on Files and what you sent). No audio or video type, and a video CV is typically 30 to 200 MB.
  • Sending. Renders come from snapshot_cv and snapshot_letter (documents/rendering.py) as a RenderedDocument carrying the same DocumentKind; uploads are attached through Application.sent_uploads.
  • Serving. serve_private_file (core/files.py) streams with Django's FileResponse, which does not answer Range requests. Fine for a PDF; for a video it means no seeking, and Safari refuses to play media at all from a server without byte ranges. The X-Accel and X-Sendfile options hand this to the web server, where it works.
  • CSP. Production sets no media-src, so default-src 'none' blocks a <video> element even from Postulo's own media; there is no frame-src either, so embedding a YouTube or Vimeo player is out — which is right, since an embed is a third-party request on every page view.
  • Export. write_archive (core/export.py, line 359) reads every file whole (handle.read()) into a BytesIO by default. A few PDFs are nothing; a handful of videos is a memory problem.

The observation names three things, and they are three different problems: a motivation letter is a kind of text, a portfolio is mostly a link, a video CV is a large binary.

Shape

1. Letters get a kind. CoverLetter.kind, with existing rows migrated to cover letter:

  • cover letter — one page, addressed, for a specific posting;
  • motivation letter — longer and sectioned, the person's story and reasons, usually with no addressee block; the norm for academic posts, EU institutions, NGOs, apprenticeships and much of the continent;
  • speculative letter — unsolicited, no posting behind it, which pairs with a thin listing (#25);
  • follow-up note — after an interview (#14): short, and people want the text back to reuse it.

Each kind has its own starter text and a sensible default theme; rendering is unchanged. DocumentKind gains motivation letter for renders and uploads. The letters page gets a kind filter, and the navigation says Letters. A translation note that matters: in French and Portuguese lettre de motivation and carta de motivação are the everyday words for a cover letter. The two kinds must be told apart by their shape in the interface — length, sections, addressee — not by their names alone, and the translators need the distinction explained in docs/TRANSLATING.md.

2. Portfolios: links, since files already exist. A Link document: title, URL, kind (portfolio, personal site, GitHub or GitLab profile, Behance or Dribbble, publication, video, other), a line of description. Links attach to applications the way uploads do (sent_links), join the CV as a Links section — the CV is built from items through a generic relation (CVItem), so this is a new item type, not a new mechanism — and travel in the export. No fetching by default: Postulo makes no request on its own. A Check links action, per link and opt-in, sends a HEAD request and reports dead ones, because a portfolio URL that 404s on the day the recruiter clicks is the worst possible outcome. Portfolio files stay uploads: the kind is already there.

3. Video CVs, in two honest steps.

  • Linked — a Link of kind video: an unlisted YouTube or Vimeo, a PeerTube instance, a Nextcloud share. Shown as a link with the provider's name; never embedded. This is what most people actually do, and it is a day's work on top of item 2.
  • Hosted — upload of mp4 and webm (H.264/AAC and VP9/Opus, which every browser plays) under its own cap, POSTULO_MAX_VIDEO_MB, default 200 and an operator's to lower: a Raspberry Pi with a small card must be able to say 50. No transcoding: no ffmpeg dependency, the person uploads what they exported, and the form says which formats play. Playback with <video controls preload="metadata"> needs two changes: media-src 'self' in the production CSP, and byte-range support in serve_private_file — or the documented recommendation to use X-Accel/X-Sendfile for instances that host video. Uploads already stream to disk (Django spills anything over 2.5 MB to a temporary file), but the reverse proxy's body limit — nginx defaults to 1 MB — must be documented on Configuration, or the first upload fails with a proxy error nobody can read.
  • Export must stream before hosted video ships: write files from disk into a temporary-file archive with ZIP_STORED for media (videos do not compress), instead of reading them into memory. Worth doing regardless of video.
  • Stores (#13) may decline a kind: Paperless is for documents, not video. The put() contract returns "not for me" and the person sees it.

4. What was sent. Today renders point at the application and uploads sit in sent_uploads. Links add sent_links. Three tables is tolerable; a single sent items table is the refactor to do if a fourth appears.

5. Documentation. Files and what you sent and Cover letters (which becomes Letters) are rewritten; Configuration gains the video cap and the proxy body-size note; seed_demo gets a motivation letter, a portfolio link and a video link so the pages show what they are for.

Classification

Enhancement. Not breaking: new kinds and one new model; existing letters migrate to cover letter; the 20 MB cap on documents stays; the CSP gains one directive.

Open questions

  1. Hosted video in 0.2.0, or links only first? Proposal: links first — it is what most people do, and it needs neither byte ranges nor a CSP change — with hosted video as the following step once the export streams.
  2. Is the follow-up note a letter (a document people reuse) or an email (which #4's SMTP notifier could one day send)? Proposal: a letter kind now; sending is a separate question.
  3. Link checking: one HEAD per link on request only (proposal), or a periodic check through the tasks framework?
## Observation > on the documents, we should introduce other kinds of documents like motivation letters / portfolios / video cvs ## What exists today - **Three shapes of document.** A **CV**, structured and rendered. A **cover letter** (`CoverLetter`: name, subject, body, template flag, theme) — one kind of letter, with no field to say which kind. An **uploaded document** with a `DocumentKind` of CV, cover letter, certificate, **portfolio**, reference or other (`documents/models.py`, line 190). So "portfolio" exists already — as a file. - **Uploads** accept `pdf doc docx odt rtf txt png jpg jpeg`, 20 MB per file (`MAX_UPLOAD_BYTES`, `documents/forms.py`; the wiki says so on *Files and what you sent*). No audio or video type, and a video CV is typically 30 to 200 MB. - **Sending.** Renders come from `snapshot_cv` and `snapshot_letter` (`documents/rendering.py`) as a `RenderedDocument` carrying the same `DocumentKind`; uploads are attached through `Application.sent_uploads`. - **Serving.** `serve_private_file` (`core/files.py`) streams with Django's `FileResponse`, which does not answer `Range` requests. Fine for a PDF; for a video it means no seeking, and Safari refuses to play media at all from a server without byte ranges. The X-Accel and X-Sendfile options hand this to the web server, where it works. - **CSP.** Production sets no `media-src`, so `default-src 'none'` blocks a `<video>` element even from Postulo's own media; there is no `frame-src` either, so embedding a YouTube or Vimeo player is out — which is right, since an embed is a third-party request on every page view. - **Export.** `write_archive` (`core/export.py`, line 359) reads every file whole (`handle.read()`) into a `BytesIO` by default. A few PDFs are nothing; a handful of videos is a memory problem. The observation names three things, and they are three different problems: a motivation letter is a **kind of text**, a portfolio is mostly a **link**, a video CV is a **large binary**. ## Shape **1. Letters get a kind.** `CoverLetter.kind`, with existing rows migrated to *cover letter*: - **cover letter** — one page, addressed, for a specific posting; - **motivation letter** — longer and sectioned, the person's story and reasons, usually with no addressee block; the norm for academic posts, EU institutions, NGOs, apprenticeships and much of the continent; - **speculative letter** — unsolicited, no posting behind it, which pairs with a thin listing (#25); - **follow-up note** — after an interview (#14): short, and people want the text back to reuse it. Each kind has its own starter text and a sensible default theme; rendering is unchanged. `DocumentKind` gains *motivation letter* for renders and uploads. The letters page gets a kind filter, and the navigation says *Letters*. **A translation note that matters:** in French and Portuguese *lettre de motivation* and *carta de motivação* are the everyday words for a cover letter. The two kinds must be told apart by their shape in the interface — length, sections, addressee — not by their names alone, and the translators need the distinction explained in `docs/TRANSLATING.md`. **2. Portfolios: links, since files already exist.** A **`Link` document**: title, URL, kind (portfolio, personal site, GitHub or GitLab profile, Behance or Dribbble, publication, video, other), a line of description. Links attach to applications the way uploads do (`sent_links`), join the CV as a *Links* section — the CV is built from items through a generic relation (`CVItem`), so this is a new item type, not a new mechanism — and travel in the export. No fetching by default: Postulo makes no request on its own. A *Check links* action, per link and opt-in, sends a HEAD request and reports dead ones, because a portfolio URL that 404s on the day the recruiter clicks is the worst possible outcome. Portfolio *files* stay uploads: the kind is already there. **3. Video CVs, in two honest steps.** - **Linked** — a `Link` of kind *video*: an unlisted YouTube or Vimeo, a PeerTube instance, a Nextcloud share. Shown as a link with the provider's name; never embedded. This is what most people actually do, and it is a day's work on top of item 2. - **Hosted** — upload of `mp4` and `webm` (H.264/AAC and VP9/Opus, which every browser plays) under its own cap, `POSTULO_MAX_VIDEO_MB`, default 200 and an operator's to lower: a Raspberry Pi with a small card must be able to say 50. **No transcoding**: no ffmpeg dependency, the person uploads what they exported, and the form says which formats play. Playback with `<video controls preload="metadata">` needs two changes: `media-src 'self'` in the production CSP, and **byte-range support in `serve_private_file`** — or the documented recommendation to use X-Accel/X-Sendfile for instances that host video. Uploads already stream to disk (Django spills anything over 2.5 MB to a temporary file), but the **reverse proxy's body limit** — nginx defaults to 1 MB — must be documented on *Configuration*, or the first upload fails with a proxy error nobody can read. - **Export must stream** before hosted video ships: write files from disk into a temporary-file archive with `ZIP_STORED` for media (videos do not compress), instead of reading them into memory. Worth doing regardless of video. - **Stores** (#13) may decline a kind: Paperless is for documents, not video. The `put()` contract returns "not for me" and the person sees it. **4. What was sent.** Today renders point at the application and uploads sit in `sent_uploads`. Links add `sent_links`. Three tables is tolerable; a single *sent items* table is the refactor to do if a fourth appears. **5. Documentation.** *Files and what you sent* and *Cover letters* (which becomes *Letters*) are rewritten; *Configuration* gains the video cap and the proxy body-size note; `seed_demo` gets a motivation letter, a portfolio link and a video link so the pages show what they are for. ## Classification Enhancement. Not breaking: new kinds and one new model; existing letters migrate to *cover letter*; the 20 MB cap on documents stays; the CSP gains one directive. ## Open questions 1. Hosted video in 0.2.0, or links only first? Proposal: links first — it is what most people do, and it needs neither byte ranges nor a CSP change — with hosted video as the following step once the export streams. 2. Is the follow-up note a letter (a document people reuse) or an email (which #4's SMTP notifier could one day send)? Proposal: a letter kind now; sending is a separate question. 3. Link checking: one HEAD per link on request only (proposal), or a periodic check through the tasks framework?
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 13:52:33 +00:00
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#28
No description provided.