Your career: references, the people who will vouch for you, kept as contacts and printed only when agreed #696
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#696
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?
The maintainer's list, under Your career, names “References”. This is the people who will vouch for a person, the referees whom an employer may ring, and not a letter of reference (that is #666). It is the one section of the family that holds data about somebody other than the account holder, so it is shaped around that: the person is a contact the instance already knows how to export and erase, a reference is printed only when they have agreed, and a CV can say “References are available on request” without naming anyone. It follows the shape and the checklist settled in #692, with the exceptions given below.
What exists today
src/postulo/resume/registry.py:28-80. A referee can be typed into a project's summary, and a CV carries no line for them.Contact: “Someone at a company: a recruiter, a hiring manager, a friend on the inside”, with a company that may be empty, a role, an email address, notes, telephone numbers, postal addresses and web links (src/postulo/jobs/models.py:604-646). Contacts at no company are written to the archive at the top level (format 20,src/postulo/core/export.py:663-669) and restored before applications read them (core/importer.py:643-645).Contact.contact_documentanswers “what do you hold on me” (src/postulo/core/gdpr.py:155-181),erase_contactdeletes the person and says what went (:277-316),retention_dry_runlists contacts older than the operator's line and what would go with them (:337-372), and the contact form shows the operator's privacy notice (GdprNoticeMixin,src/postulo/core/mixins.py:94-111). All are offered only while the data-protection feature is on (gdpr.is_offered). Merging two contacts repoints the applications that name them and refuses anything else that would be deleted with the old one (src/postulo/jobs/merging.py:630-684,_refuse_what_is_leftat:684).Contact.companyisCASCADE(src/postulo/jobs/models.py:607-613), so deleting a company deletes its contacts, and with aReferencewhosecontactis alsoCASCADEit would delete the person's referee entries too; the company's delete dialog counts postings, applications and contacts only (jobs/views.py:574-610).Application.referred_byis a contact who put a name forward for one application (#239); a referee vouches in general. The same person can be both.build_candidate_documentand the archive share one career builder (core/export.py:485-510,:868-910), so a section added toRESUME_FIELDSgoes into both files.printing.resolveand theshow_*fields onCV(src/postulo/documents/models.py:274-326), off by default for what is personal; the CV's switches are in the archive (CV_FIELDS,core/export.py:292-304), the API (api/schemas.py:758,:802,api/routers/documents.py:103,:195) and the CV form (documents/forms.py:149).RenderedDocument.source_textandplain_textare the text as sent (documents/models.py:780-793, #236), and what was sent stays true (#217), so a referee named on a CV that went out is named in its copy for good.EmploymentReferences/Referee:RefereeTypeCode(the HR-Open list Professional, Personal, Verification, or other text),PersonName,Communication,YearsKnownNumber,Comment(“Comment given by a Referee”),RoleandLink(read for #692). The plugin reads none of it (src/postulo/plugins/europass/candidate.py:136-147); a file that holds one gets the “nowhere to go” notice (:622-644). Europass's own advice, in two of its instruction sheets: “Unless the vacancy specifically requests referees, write ‘References are available on request.’” (2002-2017), and “do not give the address of a contact person without obtaining his/her formal agreement; it is preferable to state ‘References supplied on request’” (2002-2010).documents/rendering.py:117-148), and the CV picker lists every entry of a section (documents/forms.py:386-432), so an entry with acontactwould let a theme reachentry.item.contact.Checked against
mainatf09b6010b.Proposal
Three commits, each green on its own: the record, the printing, the Europass reading.
Reference(“Reference”, plural heading “References”). AResumeItemwith:contact(a required foreign key to the owner's ownContact, cascade, one entry per person: a unique constraint on owner and contact);relationship(optional text, 200: “Line manager at Aperture, 2019 to 2022”);permission(a closed list: Not asked, Asked, Agreed, default Not asked);show_details(a yes or no, off: “Print their email address and telephone number”);note(private text, never printed). The person's name, role, company, email and telephone numbers are the contact's, edited on the contact's page, which the reference form links to; a person who is not yet a contact is added there, and the contact form returns to the reference form (throughsafe_next). Slugreference, placed last inOVERVIEW_ORDERas #692 says. The form's field for the contact lists only the owner's contacts (scope_querysets, as the skill form scopes its group), and the overview row names the person, the relationship and the permission in words, so nobody reads a colour to learn what would print.contact_documentcarries the person's references (relationship, permission, whether details print, the note),DOCUMENT_VERSIONgoes up by one;erase_contactcounts them underdeletedand_kindsnames them;retention_dry_runlists them underwould_remove; the contact merge repoints them to the kept contact (or, if both are already references, keeps the kept one's entry and says so) and namesReferencein itsexpectedset; the erasure and merge wording says that CVs already sent are kept as they were sent; the company's delete dialog says how many of the contacts it removes are referees (a company delete cascades to them).show_detailsis on for the entry. The renderer does not hand a theme the entry or the contact: it hands a read-only value (name, role, organisation, relationship, and email and phone blank unless both rules allow them), so a plugin theme that writes its own loop has nocontactto read. Both base themes, the plain text and the Word file print the same lines.references_on_request, off by default so every existing CV is unchanged byte for byte (tests/test_cv_prints.py), labelled “Say that references are available on request”. It prints the sentence once, at the end, in the document's language, with or without listed references. Added to the CV form, the archive'sCV_FIELDS, the API's CV schema and the importer's CV handling, asshow_contact_detailsis (documents/models.py:274). #684 adds a CV yes-or-no throughprinting.SWITCHESinstead; the two should use one mechanism, and whichever issue lands first sets it for the other. This half could be its own issue; it is here because the two answer one question, what a CV says about referees.resume.references, each with the local id of its contact in the file (contact_id), added bybuild_documentafter_resume_block(user)returns and not inside it, because the candidate document calls_resume_blocktoo and must not carry them (core/export.py:485-510,:662,:868-910). They are restored after the contacts, as applications readreferred_by, and before the CVs, whose entries may point at them; the importer's CV-entry map (core/importer.py:905-912) gainsreference(a reference whose contact is not in the file is named in the report and skipped).FORMAT_VERSION(core/export.py:80, 29 atf09b6010b) goes up by one from whatever it is when the commit lands, once for the section and once for the CV field if they land apart.Referencestays out ofRESUME_FIELDS,RESUME_MODELSandTRANSLATION_SECTIONS, so the candidate document, its counts and its reader are unchanged andCANDIDATE_FORMAT(4) does not move. The file's page says references stay in the account, and the #692 guard names this exception and its reason.Refereeis read intoRecordas a reference:PersonNameandCommunication(an email address, a telephone number) become a contact (reusing one with the same name and address already in the account),Rolethe relationship. The permission is always Not asked, so nothing imported prints until the person has said so;RefereeTypeCode,YearsKnownNumber,CommentandLinkare not read and the review page names them. At most 20 are read, the cap the candidate reader already keeps low for telephone numbers, addresses and links (MAX_CONTACT_ROWS,resume/candidate.py:96-99), and the review page lists each person before anything is written. A telephone number goes through the same uniqueness rule and throttle as one typed on a contact (core/phone_numbers.py, #90 and #142), so a file is not a way to ask the instance which numbers it holds; a number that is refused is left out and named on the review page without saying why.core/search.py). Demo data: two, one agreed. Wiki (Your career record, CVs, and the data-protection page's contact export), anddocs/THREAT-MODEL.md: a referee's details are the account holder's data about someone else, printed only when agreed and chosen, never in a list, a log or an error message.Decisions
These are recommendations the maintainer has not confirmed.
Contact; (b) free text on the entry (name, email, telephone as typed); (c) a newRefereemodel with its own rows. Recommended: (a). The instance's export, erasure, retention and merge already know aContact, the telephone, address and link rows are already generic over it, #666 makes the writer of a reference letter aContacttoo, and the same person who vouches may refer or interview. (b) leaves nothing for an erasure to find; (c) rebuilds the contact. The cost: referees appear among the contacts, in the company page when they have one, in the API's contacts and in a contact export;Contacthas no “is a referee” mark and this issue adds none.show_detailsis a property of the entry, not of each CV, so the choice is made once. Per CV would be a column onCVItem(documents/models.py:378-413) and a control on each CV's entries. Recommended: the entry, off by default; the preview shows exactly what prints. A per-CV choice can follow if somebody wants a name on one CV and a number on another.documents/rendering.py. A theme that wants more cannot have it, which is the point.RefereeTypeCodeandYearsKnownNumberare not stored: nothing prints them, a number of years goes stale, andrelationshipcarries what the person wants said. They are named as not read on import.source_text,plain_textand the stored PDF, by the rule that what was sent stays true (#217). Recommended: do not rewrite sent documents; say so in the erasure and merge wording and in the wiki. If the maintainer wants a person scrubbed from sent copies too, that is a larger issue about what an erasure reaches and needs its own decision.EmploymentReferences.Prior art
No dependency. The standards are Europass's
Refereeelement and the HR-OpenRefereeTypeCodelist as read for #692, and Europass's advice on references and consent quoted above (the two instruction sheets are copies read in this session; where the Europass portal publishes the current wording is not verified). The note's “available on request” is the sentence Europass gives. Whether the portal writesEmploymentReferencesat all for what a person types is not verified (#318 is the check against a real download).Builds on
Issues #181 (the candidate file), #203 (the order of entries), #217 (what was sent stays true), #235 (an import does not invent a claim), #239 (who referred you), #244 and #318 (the Europass reader), #297 (the data-protection plugin and a contact's document and erasure), #308 (what a CV prints). New issues of this set: #692 (shape and checklist), #666 (reference letters, whose writer is a contact too).
Needed by
Nothing waits on it. #666 and this issue meet in the contact; neither needs the other first.
Done when
tests/test_references.py: create, edit, delete and move through the registry; the contact field lists only the owner's contacts and a foreign contact is refused by the form and by the save; one entry per person; the permission defaults to Not asked andshow_detailsto off; the overview row says the permission in words.tests/test_cv_prints.pyandtests/security/test_cv_prints.py: an entry that is not agreed prints nothing, whatever the picker holds, and the preview names it; an agreed one prints name, role, company and relationship and no email or number untilshow_detailsis on; a theme is handed nocontact(a template that reads one raises in the test theme); the plain text and the Word file match the HTML; an unchanged CV is byte for byte what it was;references_on_requestprints the sentence once, in the document's language, and is in the CV form, the archive and the API (tests/test_api.py).tests/test_gdpr.py: the contact's document lists the references, the erasure deletes and counts them and says sent copies are kept, the dry run lists them;tests/test_merging.py: a merge repoints them, keeps one entry when both are references, and no longer refuses for their sake;tests/test_jobs_isolation.pyandtests/security/test_isolation_sweep.py: another account's reference is a 404 and its contact is not offered.f09b6010b; one more than whatever it is when each commit lands),tests/test_export.pyround-trips a reference with its contact, a CV and an older archive that still loads, and a reference whose contact is missing is skipped with a line in the report; the candidate format (4) does not change, andtests/test_candidate_file.pyasserts the file carries no reference and no contact;tests/test_europass_candidate.pyreads aRefereeas above, always with Not asked, and names the elements it does not read, with a file of more than 20 referees capped; the processing record (core/gdpr.pyrecord_of_processing) is unchanged (drawn from plugins) while the document, erasure and retention are not;tests/test_wiki_surface.pyis untouched unlessRecordchanges on the surface (its wiki line and Writing a plugin follow);tests/test_page_coverage.pyneeds no entry; account deletion removes the entries (tests/test_account_deletion.py); strings are in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349).docs/THREAT-MODEL.mdnamed above and the CHANGELOG lines (each ends(#N)) are in the same commits.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 (#696).