Your details: gender, in your own words, optional, and never printed or inferred unless you say so #681

Closed
opened 2026-10-03 01:58:57 +00:00 by tiagoagueda · 0 comments
Owner

Your details has no gender field. The maintainer's list asks for one ("Add gender"). It sits beside the form of address and the pronouns that #309 added, and follows the same promises: the person gives it in their own words or not at all, nothing is ever worked out from a name, an address or any other field, and no document prints it until a CV is set to.

What exists today

  • Two neighbours already exist and are deliberately two: a form of address written before the name and pronouns that say how to refer to the person (src/postulo/accounts/models.py:265-269). accounts/addressing.py says "neither is ever worked out from a name", keeps each list per language as data and not as translations (FORMS_OF_ADDRESS, PRONOUNS: English, French, European and Brazilian Portuguese), stores "the text itself" and not a key into a list, and gives a language with no list only Other (accounts/addressing.py:1-105, #309).
  • The form is a native select (empty, the list, Other…) with a box after it that the stylesheet shows only while Other… is chosen (data-if-other), so it behaves the same with scripts off; the menu wins unless it says Other…; the options carry their language's lang and the boxes dir="auto" (accounts/forms.py: ProfileForm at :380, its _offer at :516; templates/accounts/profile.html:87-145). The blank choice reads “Not stated”.
  • No gender field exists anywhere in src. The wiki, docs/ and the README do not mention gender (searched).
  • Everything around a new profile column is #309 and #308 again: PROFILE_FIELDS and CANDIDATE_PROFILE_FIELDS (core/export.py:111, :143), FORMAT_VERSION (:80; 29 at f09b6010b) and CANDIDATE_FORMAT (:104; 4), ProfileOut and ProfilePatch with the 40-character _ADDRESSING bound (api/schemas.py:888-960), the show_* switches on CV and printing.SWITCHES (documents/printing.py:156; documents/models.py:316-326), and the contact dict (documents/rendering.py:218-229).
  • A text detail needs no special reader in the candidate file: _details cleans any string column through its own form field (resume/candidate.py:750-790).
  • vCard 4 has GENDER, whose value is a sex component (M, F, O, N or U, or empty) and, after a semicolon, free text for the identity: the grammar is sex [";" text] with sex allowed to be empty (RFC 6350 section 6.2.7). The Europass Candidate XML the importer is tested with carries GenderCode directly under Person, a value of HR-Open Standards' GenderEnumType (the sample holds “Male”; rule BR-CV-10 in its comments; tests/data/europass-candidate-sample.xml:98-100); the reader here takes none (plugins/europass/candidate.py:345-398).
  • HTML has an autofill token for it, sex ("gender identity, free-form text"), which WCAG 2.1 SC 1.3.5 lists among the input purposes a field may identify itself with.

Checked against main at f09b6010b.

Proposal

  1. A gender column on Profile: optional text of at most 40 characters, the person's own words, exactly as the form of address is stored. Blank means nothing is said; nothing is assumed.
  2. The control, in the card #679 makes: a select with “Not stated”, the list for the language of the career record (as addressing.forms_of_address picks), and Other… with its box, using the machinery of ProfileForm._offer and the data-if-other rule, so it works with scripts off. A short list per language, in accounts/addressing.py: English “Woman”, “Man”, “Non-binary”; French and the two Portuguese equivalents; each list with its source named in the module docstring, as the others are, and a language with no list offers Other alone. autocomplete="sex" on the field.
  3. Independent of the two next to it. Setting a gender never sets, narrows or suggests a form of address or pronouns, and the reverse; no code reads one to decide another, and no code reads any of them to decide anything but what a CV prints. A test holds it.
  4. Printed only where a CV says so: show_gender on CV, off, in printing.SWITCHES, so the form, the API and the importer pick it up; the contact dict gains gender; the base themes, the plain text and the Word file print it as one detail (“Gender: …”) among the personal ones, and a plugin theme sees a key it may ignore.
  5. Archive, candidate file, API. The column joins PROFILE_FIELDS and CANDIDATE_PROFILE_FIELDS; each format is one more than it is when the commit lands, with its history line. A candidate file's value is read "filled where blank, kept where not", like the form of address. ProfileOut and ProfilePatch take it with the page's rules (stripped, held to 40, a NUL is a 422 naming the field); CVPrintsOut/CVPrintsIn gain the boolean.
  6. Not in this issue: mapping to vCard GENDER or to Europass. They belong with #660, and the mapping rule is recorded below so it is not decided by accident.

Decisions

  • What to call it, and how much to offer. “Gender” is the list's word. A list that offers only two options, or a required choice, would be wrong for many people and for the promise of #309. Recommended: the label “Gender”, an optional field, a short list per language of what is in common use, Other… for anything else, “Not stated” as the empty answer. Every list is a draft until a speaker of the language has read it, like every other translated string (docs/TRANSLATING.md).
  • Stored as the text, not as a code. #309 settled it for the form of address and pronouns: the text survives a change of language and an Other is just text. Recommended: the same here. The cost is that a code-based exchange format (vCard's M/F/O/N/U) has nothing to take straight from a stored word.
  • How it maps to vCard later. vCard separates a sex code from a free-text identity, and its grammar allows the code to be empty. Recommended: write the stored word as the identity text with an empty sex component (GENDER:;Non-binary); never fill the sex component from the word; and when reading a card, never set the field from GENDER without the person confirming on a review page. Decided here so #660 does not coerce a word into M or F. For a Europass writer, a word that is not one of GenderEnumType's values (the list was not checked here) is left out rather than forced.
  • On a CV: yes or no. Default off, as in #308, so a CV prints none until it is set to. Some jurisdictions advise against stating it and others expect it; the switch is the whole answer. Recommended: off, one plain switch, no wording that suggests it should be turned on.
  • In the candidate file and the API. As for #679: carried, under read and write, and never in a list endpoint, a webhook, a log, a search result or an error message.
  • Legal status. Gender is personal data. Article 9(1) of the GDPR lists sex life and sexual orientation, not gender, and some regulators treat it as sensitive in practice; the maintainer should confirm the reading for any instance that must answer to one. The treatment is the same whichever way it falls: optional, off on every CV, never inferred, removed with the account, named in docs/THREAT-MODEL.md.
  • Contacts. The field is for the account holder's own details. Recommended: no gender on a contact, which would be recording something about another person that nobody asked to be recorded and that no job-search record needs.

Builds on

#679 (the card, the switches, the validator pattern, the format bumps), #309 (form of address and pronouns: separate, never inferred), #308 (a CV chooses what it prints), #181 (the candidate file), #301 (the native select stays the scripts-off path). #660 for the vCard side.

Done when

  • A new test file tests/test_gender.py (or cases in tests/test_form_of_address.py, beside the two it sits with): the field saves a listed value, an Other text and a clear; a value outside the list is kept as text (the menu accepts what it is sent, as #309 does); the length bound and NUL refusal; setting it changes neither the form of address nor the pronouns, and the reverse.
  • tests/test_cv_prints.py and tests/security/test_cv_prints.py cover show_gender (default off, chosen on, the archive carrying it); nothing is printed by default in the preview, the plain text or the Word file; tests/test_export.py and the archive round trip carry the column at the new format; tests/test_candidate_file.py holds the new fingerprint and a review row; the archive importer reads it through the column's rule (tests/test_importers.py).
  • tests/test_api.py: GET and PATCH /profile carry gender with the page's bounds; the CV's prints carries the boolean. Search is unaffected: nothing here is indexed (core/search.py has no profile group), and a test says so for the new fields.
  • The account-deletion test and the isolation sweep show it gone with the account; core/gdpr.py needs no line, because the record of processing is drawn from plugins and connections.
  • Accessibility: the field has a visible label, the sex autocomplete token, a described-by sentence, and the options carry their lang; axe is clean in both themes (tests/e2e/test_accessibility.py, tests/e2e/test_form_of_address.py extended).
  • The strings and the per-language lists are in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and in Weblate (#349); the wiki's Getting started (which explains the name's companions), CVs and The capture API and docs/THREAT-MODEL.md change in the same commit.

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

Your details has no gender field. The maintainer's list asks for one ("Add gender"). It sits beside the form of address and the pronouns that #309 added, and follows the same promises: the person gives it in their own words or not at all, nothing is ever worked out from a name, an address or any other field, and no document prints it until a CV is set to. ## What exists today - Two neighbours already exist and are deliberately two: a **form of address** written before the name and **pronouns** that say how to refer to the person (`src/postulo/accounts/models.py:265-269`). `accounts/addressing.py` says "neither is ever worked out from a name", keeps each list per language as data and not as translations (`FORMS_OF_ADDRESS`, `PRONOUNS`: English, French, European and Brazilian Portuguese), stores "the text itself" and not a key into a list, and gives a language with no list only *Other* (`accounts/addressing.py:1-105`, #309). - The form is a native select (empty, the list, *Other…*) with a box after it that the stylesheet shows only while *Other…* is chosen (`data-if-other`), so it behaves the same with scripts off; the menu wins unless it says *Other…*; the options carry their language's `lang` and the boxes `dir="auto"` (`accounts/forms.py`: `ProfileForm` at `:380`, its `_offer` at `:516`; `templates/accounts/profile.html:87-145`). The blank choice reads “Not stated”. - No gender field exists anywhere in `src`. The wiki, `docs/` and the README do not mention gender (searched). - Everything around a new profile column is #309 and #308 again: `PROFILE_FIELDS` and `CANDIDATE_PROFILE_FIELDS` (`core/export.py:111`, `:143`), `FORMAT_VERSION` (`:80`; 29 at `f09b6010b`) and `CANDIDATE_FORMAT` (`:104`; 4), `ProfileOut` and `ProfilePatch` with the 40-character `_ADDRESSING` bound (`api/schemas.py:888-960`), the `show_*` switches on `CV` and `printing.SWITCHES` (`documents/printing.py:156`; `documents/models.py:316-326`), and the `contact` dict (`documents/rendering.py:218-229`). - A text detail needs no special reader in the candidate file: `_details` cleans any string column through its own form field (`resume/candidate.py:750-790`). - vCard 4 has `GENDER`, whose value is a sex component (`M`, `F`, `O`, `N` or `U`, or empty) and, after a semicolon, free text for the identity: the grammar is `sex [";" text]` with `sex` allowed to be empty (RFC 6350 section 6.2.7). The Europass Candidate XML the importer is tested with carries `GenderCode` directly under `Person`, a value of HR-Open Standards' `GenderEnumType` (the sample holds “Male”; rule BR-CV-10 in its comments; `tests/data/europass-candidate-sample.xml:98-100`); the reader here takes none (`plugins/europass/candidate.py:345-398`). - HTML has an autofill token for it, `sex` ("gender identity, free-form text"), which WCAG 2.1 SC 1.3.5 lists among the input purposes a field may identify itself with. Checked against `main` at `f09b6010b`. ## Proposal 1. **A `gender` column on `Profile`**: optional text of at most 40 characters, the person's own words, exactly as the form of address is stored. Blank means nothing is said; nothing is assumed. 2. **The control, in the card #679 makes**: a select with “Not stated”, the list for the language of the career record (as `addressing.forms_of_address` picks), and *Other…* with its box, using the machinery of `ProfileForm._offer` and the `data-if-other` rule, so it works with scripts off. A short list per language, in `accounts/addressing.py`: English “Woman”, “Man”, “Non-binary”; French and the two Portuguese equivalents; each list with its source named in the module docstring, as the others are, and a language with no list offers *Other* alone. `autocomplete="sex"` on the field. 3. **Independent of the two next to it.** Setting a gender never sets, narrows or suggests a form of address or pronouns, and the reverse; no code reads one to decide another, and no code reads any of them to decide anything but what a CV prints. A test holds it. 4. **Printed only where a CV says so**: `show_gender` on `CV`, off, in `printing.SWITCHES`, so the form, the API and the importer pick it up; the `contact` dict gains `gender`; the base themes, the plain text and the Word file print it as one detail (“Gender: …”) among the personal ones, and a plugin theme sees a key it may ignore. 5. **Archive, candidate file, API.** The column joins `PROFILE_FIELDS` and `CANDIDATE_PROFILE_FIELDS`; each format is one more than it is when the commit lands, with its history line. A candidate file's value is read "filled where blank, kept where not", like the form of address. `ProfileOut` and `ProfilePatch` take it with the page's rules (stripped, held to 40, a NUL is a 422 naming the field); `CVPrintsOut`/`CVPrintsIn` gain the boolean. 6. **Not in this issue:** mapping to vCard `GENDER` or to Europass. They belong with #660, and the mapping rule is recorded below so it is not decided by accident. ## Decisions - **What to call it, and how much to offer.** “Gender” is the list's word. A list that offers only two options, or a required choice, would be wrong for many people and for the promise of #309. **Recommended:** the label “Gender”, an optional field, a short list per language of what is in common use, *Other…* for anything else, “Not stated” as the empty answer. Every list is a draft until a speaker of the language has read it, like every other translated string (`docs/TRANSLATING.md`). - **Stored as the text, not as a code.** #309 settled it for the form of address and pronouns: the text survives a change of language and an *Other* is just text. **Recommended:** the same here. The cost is that a code-based exchange format (vCard's `M`/`F`/`O`/`N`/`U`) has nothing to take straight from a stored word. - **How it maps to vCard later.** vCard separates a sex code from a free-text identity, and its grammar allows the code to be empty. **Recommended:** write the stored word as the identity text with an empty sex component (`GENDER:;Non-binary`); never fill the sex component from the word; and when reading a card, never set the field from `GENDER` without the person confirming on a review page. Decided here so #660 does not coerce a word into `M` or `F`. For a Europass writer, a word that is not one of `GenderEnumType`'s values (the list was not checked here) is left out rather than forced. - **On a CV: yes or no.** Default off, as in #308, so a CV prints none until it is set to. Some jurisdictions advise against stating it and others expect it; the switch is the whole answer. **Recommended:** off, one plain switch, no wording that suggests it should be turned on. - **In the candidate file and the API.** As for #679: carried, under `read` and `write`, and never in a list endpoint, a webhook, a log, a search result or an error message. - **Legal status.** Gender is personal data. Article 9(1) of the GDPR lists sex life and sexual orientation, not gender, and some regulators treat it as sensitive in practice; the maintainer should confirm the reading for any instance that must answer to one. The treatment is the same whichever way it falls: optional, off on every CV, never inferred, removed with the account, named in `docs/THREAT-MODEL.md`. - **Contacts.** The field is for the account holder's own details. **Recommended:** no gender on a contact, which would be recording something about another person that nobody asked to be recorded and that no job-search record needs. ## Builds on #679 (the card, the switches, the validator pattern, the format bumps), #309 (form of address and pronouns: separate, never inferred), #308 (a CV chooses what it prints), #181 (the candidate file), #301 (the native select stays the scripts-off path). #660 for the vCard side. ## Done when - A new test file `tests/test_gender.py` (or cases in `tests/test_form_of_address.py`, beside the two it sits with): the field saves a listed value, an *Other* text and a clear; a value outside the list is kept as text (the menu accepts what it is sent, as #309 does); the length bound and NUL refusal; setting it changes neither the form of address nor the pronouns, and the reverse. - `tests/test_cv_prints.py` and `tests/security/test_cv_prints.py` cover `show_gender` (default off, chosen on, the archive carrying it); nothing is printed by default in the preview, the plain text or the Word file; `tests/test_export.py` and the archive round trip carry the column at the new format; `tests/test_candidate_file.py` holds the new fingerprint and a review row; the archive importer reads it through the column's rule (`tests/test_importers.py`). - `tests/test_api.py`: `GET` and `PATCH /profile` carry `gender` with the page's bounds; the CV's `prints` carries the boolean. Search is unaffected: nothing here is indexed (`core/search.py` has no profile group), and a test says so for the new fields. - The account-deletion test and the isolation sweep show it gone with the account; `core/gdpr.py` needs no line, because the record of processing is drawn from plugins and connections. - Accessibility: the field has a visible label, the `sex` autocomplete token, a described-by sentence, and the options carry their `lang`; axe is clean in both themes (`tests/e2e/test_accessibility.py`, `tests/e2e/test_form_of_address.py` extended). - The strings and the per-language lists are in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and in Weblate (#349); the wiki's *Getting started* (which explains the name's companions), *CVs* and *The capture API* and `docs/THREAT-MODEL.md` change in the same commit. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.6.0 milestone 2026-10-03 01:58:57 +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#681
No description provided.