A CV photo of its own, independent of the avatar, printed only where a CV says so #668
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#668
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the list of 2026-10-02: "A CV photo may not be the avatar photo, make them independent." No CV prints a photograph today, and the picture a person has on their profile is the wrong picture for one: it is a square crop made for a header tile, and it may be an illustration or a holiday snapshot. The wiki has already promised that a photo on a CV "will be a per-CV choice, off by default, when it arrives" (
Accounts-and-invitations.md:42-43). This is that, with a photo of its own.What exists today
contact_detailshands a document the name, headline, email, phone, location, links, identifiers, form of address and pronouns, and no picture (src/postulo/documents/rendering.py:151-231). The two built-in themes and the shared base print none (templates/documents/themes/base_cv.html:67-80,plain/cv.html,classic/cv.html,base_portfolio.html); a search ofsrc/postulofor "photo" finds comments only. The templates plugin records the same gap: "contactcarries no picture, so no template can print one … a CV with a photograph is a core change" (postulo/postulo-templates#2).accounts/avatars.py:67-80,core/pictures.py:115-131,square=TruethroughImageOps.fit), stored at most 1 MiB underavatars/<pk>/(accounts/models.py:152-154,:410), served to the person and to administrators (accounts/views.py:297-315), and meant for the interface. A passport-style photograph is 35 x 45 mm, about 413 x 531 pixels at 300 dpi (accounts/avatars.py:40-45, #265), which is portrait, so a square crop has already thrown part of it away. The wiki says "Nothing here appears on a CV" (Accounts-and-invitations.md:42).QUIETnames "the photograph, which the old reader never took either" (plugins/europass/candidate.py:149-150). The candidate file leaves the picture out on purpose and says so (Your-career-record.md:529-534).printing.SWITCHES(documents/printing.py:153-160):show_location,show_form_of_addressandshow_pronouns(documents/models.py:316-326). That one dictionary is read by the CV form (documents/forms.py:294), the API (api/routers/documents.py:78,:191), the archive (documents/printing.py:550,:636) and the importer (core/importer.py:955).printing.resolvecomputes what is printed (:340-385), andcontact_detailsis "the only place they reach a document" (rendering.py:151-166), so the CV, the preview, the PDF and a theme nobody here has seen all receive the same answer. The two #309 added (show_form_of_address,show_pronouns) default to off, "until a CV says so".data:addresses and nothing else (documents/pdf.py:231-236), and the page's policy allows images from'self'anddata:only (config/settings/base.py:681). A photo can only reach a document as adata:value.documents/docx.py,documents/formats.py), not from a template.Checked against
mainatf09b6010b.Proposal
core.pictures.as_stored), but not cropped: the person's own framing is kept, within the same 1 MiB budget and 5 MB input cap. Stored the way #662 decides for the avatar, and it follows the same rule: it goes with its owner.show_photojoinsprinting.SWITCHES, default off, so the form, the API, the archive and the importer carry it with no further code. It lives inside the contact block: a CV whose "include contact details" switch is off prints no photo, as it prints no name.contact_detailscarriesphoto, adata:address; otherwise an empty string.base_cv.htmlandbase_portfolio.htmlprint it in the header in a 35 x 45 mm frame (object-fit: cover, top-weighted, which WeasyPrint draws and a browser preview draws the same), withalt=""and beside the name in the document order.plainandclassicstyle the frame. A theme that does not readcontact.photoprints no photo and is not broken (keys a theme was written against keep their meaning, asrendering.py:186-199already says).contact.photo, and that a theme may ignore it).Decisions
documents/docx.py, which is deliberately small). Recommended: not in this issue; say so on the page.Builds on
#308 and #309 (closed), #265 (closed), #482 (the same re-encoding path), #662 (where the picture lives), postulo/postulo-templates#2.
Needed by
postulo/postulo-templates#2 (a Europass-style template that prints a photograph), the plugin's own theme issues.
Done when
tests/test_cv_photo.py: a photo is stored independently of the avatar (changing one leaves the other); metadata is stripped; a file that is not an image is refused; an uploaded photo is not cropped; deleting the account removes it and its file; a CV withshow_photooff hands the renderer an emptyphoto, one with it on adata:address, and the master switch off removes it.tests/test_cv_prints.py: the new switch is carried by the form, the API, the archive and the importer, and an older archive restores with it off; a changed switch makes a new version of what was sent (the existing rule), and a sent PDF is unchanged afterwards.tests/test_document_themes.pyandtests/test_cv_prints.py: both built-in themes print the frame when given a photo and are byte for byte what they were without one; the browser preview shows it under the real policy with no violation (tests/e2e/), axe-clean in both themes.tests/security/test_cv_prints.py: nobody can name or read another person's photo through the CV form or the API.core/export.pyFORMAT_VERSION, 29 onf09b6010b; one more than whatever it is when the commit lands) with a round trip; candidate format (4) unchanged and the page that says the picture is left out still true; API:prints.photoon a CV (tests/test_api.py); search unaffected; CV print choice is this issue; processing record (core/gdpr.py, drawn from plugins and connections) needs nothing;tests/test_wiki_surface.pyis for names onpostulo.plugins.api, and none is added (contact.photois a context key, documented in the wiki); new pages, if any, intests/test_page_coverage.pyandtests/security/test_isolation_sweep.py; 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.
Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#668).