Your career: references, the people who will vouch for you, kept as contacts and printed only when agreed #696

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

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

  • No such section. The career's kinds are in 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.
  • A person other than the account holder is a 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).
  • The instance already has duties for such a person, and they are keyed on Contact. contact_document answers “what do you hold on me” (src/postulo/core/gdpr.py:155-181), erase_contact deletes the person and says what went (:277-316), retention_dry_run lists 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_left at :684).
  • A contact goes with its company. Contact.company is CASCADE (src/postulo/jobs/models.py:607-613), so deleting a company deletes its contacts, and with a Reference whose contact is also CASCADE it would delete the person's referee entries too; the company's delete dialog counts postings, applications and contacts only (jobs/views.py:574-610).
  • Who referred you is a different act. Application.referred_by is a contact who put a name forward for one application (#239); a referee vouches in general. The same person can be both.
  • The candidate file carries no contact. It is “the part of an account that is the person” (#181): the profile, its rows, the identifiers and the career. build_candidate_document and the archive share one career builder (core/export.py:485-510, :868-910), so a section added to RESUME_FIELDS goes into both files.
  • What a CV is allowed to say about a person is a CV's own choice (#308): printing.resolve and the show_* fields on CV (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).
  • A sent CV keeps its words. RenderedDocument.source_text and plain_text are 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.
  • What Europass says. The Candidate format has EmploymentReferences/Referee: RefereeTypeCode (the HR-Open list Professional, Personal, Verification, or other text), PersonName, Communication, YearsKnownNumber, Comment (“Comment given by a Referee”), Role and Link (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).
  • A theme is handed the entry itself (documents/rendering.py:117-148), and the CV picker lists every entry of a section (documents/forms.py:386-432), so an entry with a contact would let a theme reach entry.item.contact.

Checked against main at f09b6010b.

Proposal

Three commits, each green on its own: the record, the printing, the Europass reading.

  1. The record, Reference (“Reference”, plural heading “References”). A ResumeItem with: contact (a required foreign key to the owner's own Contact, 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 (through safe_next). Slug reference, placed last in OVERVIEW_ORDER as #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.
  2. The contact's rights follow the contact. contact_document carries the person's references (relationship, permission, whether details print, the note), DOCUMENT_VERSION goes up by one; erase_contact counts them under deleted and _kinds names them; retention_dry_run lists them under would_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 names Reference in its expected set; 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).
  3. What a CV prints. A reference is printed only when its permission is Agreed. An entry that is not agreed is left off the CV whatever the picker says, the picker shows it with the reason, and the preview says which entries were left out. An agreed reference prints the person's name, their role and company, and the relationship; the email address and the primary telephone number only when show_details is 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 no contact to read. Both base themes, the plain text and the Word file print the same lines.
  4. “References are available on request.” A yes or no on the CV, 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's CV_FIELDS, the API's CV schema and the importer's CV handling, as show_contact_details is (documents/models.py:274). #684 adds a CV yes-or-no through printing.SWITCHES instead; 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.
  5. Archive. The references are written by their own function under resume.references, each with the local id of its contact in the file (contact_id), added by build_document after _resume_block(user) returns and not inside it, because the candidate document calls _resume_block too and must not carry them (core/export.py:485-510, :662, :868-910). They are restored after the contacts, as applications read referred_by, and before the CVs, whose entries may point at them; the importer's CV-entry map (core/importer.py:905-912) gains reference (a reference whose contact is not in the file is named in the report and skipped). FORMAT_VERSION (core/export.py:80, 29 at f09b6010b) 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.
  6. The candidate file does not carry them. Reference stays out of RESUME_FIELDS, RESUME_MODELS and TRANSLATION_SECTIONS, so the candidate document, its counts and its reader are unchanged and CANDIDATE_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.
  7. Europass. Referee is read into Record as a reference: PersonName and Communication (an email address, a telephone number) become a contact (reusing one with the same name and address already in the account), Role the relationship. The permission is always Not asked, so nothing imported prints until the person has said so; RefereeTypeCode, YearsKnownNumber, Comment and Link are 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.
  8. Search does not index references: the person is found as a contact (core/search.py). Demo data: two, one agreed. Wiki (Your career record, CVs, and the data-protection page's contact export), and docs/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.

  • Where the person lives. Options: (a) a Contact; (b) free text on the entry (name, email, telephone as typed); (c) a new Referee model with its own rows. Recommended: (a). The instance's export, erasure, retention and merge already know a Contact, the telephone, address and link rows are already generic over it, #666 makes the writer of a reference letter a Contact too, 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; Contact has no “is a referee” mark and this issue adds none.
  • What permission means. A three-step note of the person's own: not asked, asked, agreed. It is Europass's own advice, not a legal record: it does not discharge the instance operator's duties, and it is not a consent form. Recommended: keep it that plain. The alternative is a dated consent record with who confirmed and how, which asks for more than the person has to give and invites the question of who keeps it.
  • A reference that is not agreed. Recommended: never printed, name included. Listing someone as a referee says they agreed to be one. The picker still shows the entry, greyed with the reason, so it is not mysteriously missing.
  • Details: entry or CV. show_details is a property of the entry, not of each CV, so the choice is made once. Per CV would be a column on CVItem (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.
  • The theme gets a value, not the contact. Recommended: yes, so the two rules cannot be bypassed by a theme from a plugin; it costs one small dataclass in documents/rendering.py. A theme that wants more cannot have it, which is the point.
  • The candidate file. Options: (a) not carried, as chosen; (b) carried with the person's details inline, creating contacts on import; (c) carried without email or telephone, so the person would have to re-add them. Recommended: (a). The file is for moving the person's own career and the whole archive is for moving everything; a file handed to another instance should not hold other people's addresses, and (b) would have a stranger's file create contacts. It costs the guard a named exception and the file page a sentence.
  • Types and years known. Europass's RefereeTypeCode and YearsKnownNumber are not stored: nothing prints them, a number of years goes stale, and relationship carries what the person wants said. They are named as not read on import.
  • Translations. Recommended: translates nothing. The candidate file has no block for references, so a translation row would point at nothing, and the relationship is short; a person writing for a French CV writes it in French, or leaves it empty.
  • Sent copies. A CV that named a referee keeps them in source_text, plain_text and 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.
  • The sentence. “References are available on request.” is Europass's wording and the maintainer's “available on request”. Recommended: the one message, translated through the catalogues; the CV switch is a yes or no and prints no names.
  • Importing from a Europass file creates contacts from a stranger's file. Recommended: yes, limited as above, listed on the review page, permission Not asked, because an import that invented an agreement would be the #235 mistake again. If the maintainer prefers, this commit waits, and the notice keeps naming EmploymentReferences.

Prior art

No dependency. The standards are Europass's Referee element and the HR-Open RefereeTypeCode list 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 writes EmploymentReferences at 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

  • A new 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 and show_details to off; the overview row says the permission in words.
  • CV printing, tests/test_cv_prints.py and tests/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 until show_details is on; a theme is handed no contact (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_request prints 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.py and tests/security/test_isolation_sweep.py: another account's reference is a 404 and its contact is not offered.
  • The standing checklist, adjusted: the archive format goes up by one (29 at f09b6010b; one more than whatever it is when each commit lands), tests/test_export.py round-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, and tests/test_candidate_file.py asserts the file carries no reference and no contact; tests/test_europass_candidate.py reads a Referee as 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.py record_of_processing) is unchanged (drawn from plugins) while the document, erasure and retention are not; tests/test_wiki_surface.py is untouched unless Record changes on the surface (its wiki line and Writing a plugin follow); tests/test_page_coverage.py needs 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).
  • The wiki pages and docs/THREAT-MODEL.md named above and the CHANGELOG lines (each ends (#N)) are in the same commits.

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

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 - **No such section.** The career's kinds are in `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. - **A person other than the account holder is a `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`). - **The instance already has duties for such a person, and they are keyed on `Contact`.** `contact_document` answers “what do you hold on me” (`src/postulo/core/gdpr.py:155-181`), `erase_contact` deletes the person and says what went (`:277-316`), `retention_dry_run` lists 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_left` at `:684`). - **A contact goes with its company.** `Contact.company` is `CASCADE` (`src/postulo/jobs/models.py:607-613`), so deleting a company deletes its contacts, and with a `Reference` whose `contact` is also `CASCADE` it would delete the person's referee entries too; the company's delete dialog counts postings, applications and contacts only (`jobs/views.py:574-610`). - **Who referred you is a different act.** `Application.referred_by` is a contact who put a name forward for one application (#239); a referee vouches in general. The same person can be both. - **The candidate file carries no contact.** It is “the part of an account that is the person” (#181): the profile, its rows, the identifiers and the career. `build_candidate_document` and the archive share one career builder (`core/export.py:485-510`, `:868-910`), so a section added to `RESUME_FIELDS` goes into both files. - **What a CV is allowed to say about a person is a CV's own choice** (#308): `printing.resolve` and the `show_*` fields on `CV` (`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`). - **A sent CV keeps its words.** `RenderedDocument.source_text` and `plain_text` are 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. - **What Europass says.** The Candidate format has `EmploymentReferences/Referee`: `RefereeTypeCode` (the HR-Open list *Professional*, *Personal*, *Verification*, or other text), `PersonName`, `Communication`, `YearsKnownNumber`, `Comment` (“Comment given by a Referee”), `Role` and `Link` (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). - **A theme is handed the entry itself** (`documents/rendering.py:117-148`), and the CV picker lists every entry of a section (`documents/forms.py:386-432`), so an entry with a `contact` would let a theme reach `entry.item.contact`. Checked against `main` at `f09b6010b`. ## Proposal Three commits, each green on its own: the record, the printing, the Europass reading. 1. **The record, `Reference`** (“Reference”, plural heading “References”). A `ResumeItem` with: `contact` (a required foreign key to the owner's own `Contact`, 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 (through `safe_next`). Slug `reference`, placed last in `OVERVIEW_ORDER` as #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. 2. **The contact's rights follow the contact.** `contact_document` carries the person's references (relationship, permission, whether details print, the note), `DOCUMENT_VERSION` goes up by one; `erase_contact` counts them under `deleted` and `_kinds` names them; `retention_dry_run` lists them under `would_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 names `Reference` in its `expected` set; 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). 3. **What a CV prints.** A reference is printed only when its permission is *Agreed*. An entry that is not agreed is left off the CV whatever the picker says, the picker shows it with the reason, and the preview says which entries were left out. An agreed reference prints the person's name, their role and company, and the relationship; the email address and the primary telephone number only when `show_details` is 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 no `contact` to read. Both base themes, the plain text and the Word file print the same lines. 4. **“References are available on request.”** A yes or no on the CV, `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's `CV_FIELDS`, the API's CV schema and the importer's CV handling, as `show_contact_details` is (`documents/models.py:274`). #684 adds a CV yes-or-no through `printing.SWITCHES` instead; 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. 5. **Archive.** The references are written by their own function under `resume.references`, each with the local id of its contact in the file (`contact_id`), added by `build_document` after `_resume_block(user)` returns and not inside it, because the candidate document calls `_resume_block` too and must not carry them (`core/export.py:485-510`, `:662`, `:868-910`). They are restored after the contacts, as applications read `referred_by`, and before the CVs, whose entries may point at them; the importer's CV-entry map (`core/importer.py:905-912`) gains `reference` (a reference whose contact is not in the file is named in the report and skipped). `FORMAT_VERSION` (`core/export.py:80`, 29 at `f09b6010b`) 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. 6. **The candidate file does not carry them.** `Reference` stays out of `RESUME_FIELDS`, `RESUME_MODELS` and `TRANSLATION_SECTIONS`, so the candidate document, its counts and its reader are unchanged and `CANDIDATE_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. 7. **Europass.** `Referee` is read into `Record` as a reference: `PersonName` and `Communication` (an email address, a telephone number) become a contact (reusing one with the same name and address already in the account), `Role` the relationship. **The permission is always *Not asked***, so nothing imported prints until the person has said so; `RefereeTypeCode`, `YearsKnownNumber`, `Comment` and `Link` are 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. 8. **Search** does not index references: the person is found as a contact (`core/search.py`). **Demo data**: two, one agreed. **Wiki** (*Your career record*, *CVs*, and the data-protection page's contact export), and **`docs/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. - **Where the person lives.** Options: (a) a `Contact`; (b) free text on the entry (name, email, telephone as typed); (c) a new `Referee` model with its own rows. **Recommended: (a).** The instance's export, erasure, retention and merge already know a `Contact`, the telephone, address and link rows are already generic over it, #666 makes the writer of a reference letter a `Contact` too, 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; `Contact` has no “is a referee” mark and this issue adds none. - **What permission means.** A three-step note of the person's own: not asked, asked, agreed. It is Europass's own advice, not a legal record: it does not discharge the instance operator's duties, and it is not a consent form. **Recommended:** keep it that plain. The alternative is a dated consent record with who confirmed and how, which asks for more than the person has to give and invites the question of who keeps it. - **A reference that is not agreed.** **Recommended: never printed, name included.** Listing someone as a referee says they agreed to be one. The picker still shows the entry, greyed with the reason, so it is not mysteriously missing. - **Details: entry or CV.** `show_details` is a property of the entry, not of each CV, so the choice is made once. Per CV would be a column on `CVItem` (`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. - **The theme gets a value, not the contact.** **Recommended: yes**, so the two rules cannot be bypassed by a theme from a plugin; it costs one small dataclass in `documents/rendering.py`. A theme that wants more cannot have it, which is the point. - **The candidate file.** Options: (a) not carried, as chosen; (b) carried with the person's details inline, creating contacts on import; (c) carried without email or telephone, so the person would have to re-add them. **Recommended: (a).** The file is for moving the person's own career and the whole archive is for moving everything; a file handed to another instance should not hold other people's addresses, and (b) would have a stranger's file create contacts. It costs the guard a named exception and the file page a sentence. - **Types and years known.** Europass's `RefereeTypeCode` and `YearsKnownNumber` are not stored: nothing prints them, a number of years goes stale, and `relationship` carries what the person wants said. They are named as not read on import. - **Translations.** **Recommended: translates nothing.** The candidate file has no block for references, so a translation row would point at nothing, and the relationship is short; a person writing for a French CV writes it in French, or leaves it empty. - **Sent copies.** A CV that named a referee keeps them in `source_text`, `plain_text` and 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. - **The sentence.** “References are available on request.” is Europass's wording and the maintainer's “available on request”. **Recommended:** the one message, translated through the catalogues; the CV switch is a yes or no and prints no names. - **Importing from a Europass file** creates contacts from a stranger's file. **Recommended:** yes, limited as above, listed on the review page, permission *Not asked*, because an import that invented an agreement would be the #235 mistake again. If the maintainer prefers, this commit waits, and the notice keeps naming `EmploymentReferences`. ## Prior art No dependency. The standards are Europass's `Referee` element and the HR-Open `RefereeTypeCode` list 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 writes `EmploymentReferences` at 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 - A new `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* and `show_details` to off; the overview row says the permission in words. - CV printing, `tests/test_cv_prints.py` and `tests/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 until `show_details` is on; a theme is handed no `contact` (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_request` prints 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.py` and `tests/security/test_isolation_sweep.py`: another account's reference is a 404 and its contact is not offered. - The standing checklist, adjusted: the archive format goes up by one (29 at `f09b6010b`; one more than whatever it is when each commit lands), `tests/test_export.py` round-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, and `tests/test_candidate_file.py` asserts the file carries no reference and no contact; `tests/test_europass_candidate.py` reads a `Referee` as 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.py` `record_of_processing`) is unchanged (drawn from plugins) while the document, erasure and retention are not; `tests/test_wiki_surface.py` is untouched unless `Record` changes on the surface (its wiki line and *Writing a plugin* follow); `tests/test_page_coverage.py` needs 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). - The wiki pages and `docs/THREAT-MODEL.md` named above and the CHANGELOG lines (each ends `(#N)`) are in the same commits. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.6.0 milestone 2026-10-03 01:59:07 +00:00
Author
Owner

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

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