Your career: honours and awards, and memberships #693

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

The maintainer's list, under Your career, names “Honours and awards” and “Memberships” among the additional sections. Postulo has a place for neither: a prize can only be typed into a project's summary and a membership into a skill group. This issue adds the two as sections of the career record, each a small typed entry, following the shape and the checklist settled in #692.

What exists today

  • Neither exists. A search of src/postulo for honours, awards and membership finds no model, field or string for a career: the matches are the words in other senses (“Postulo honours robots.txt”, “membership of ISO 4217”). The career's eight kinds are in src/postulo/resume/registry.py:28-80.
  • The nearest section is Certification: a name, an issuer, an issue date, an optional expiry and a credential link (src/postulo/resume/models.py:215-228). Its issuer is free text, its name is not translatable, and a CV prints it as “name, issuer” and the year (src/postulo/templates/documents/themes/base_cv.html:117-125, src/postulo/documents/rendering.py:363-371). An honour reads the same way on a CV and differs in what it is: recognition, with nothing to check and no expiry.
  • A degree's classification is already somewhere: Education.grade is “as the institution words it” (resume/forms.py:122), so “First-class honours” and “cum laude” have a home that is not this section.
  • Organisations are free text on purpose (Experience.organisation, resume/models.py:65-74, #131; resume/translatable.py:16-22: “Universidade de Lisboa stays that on an English CV”), and #683 is reopening that for employers. The company list is the one an application's picker draws on.
  • A new entry is placed by its date where its section has one: DATE_FIELDS names the field for experience, education and certifications (src/postulo/resume/ordering.py:28-32; the candidate import's _place reads it too, resume/candidate.py:1382), and the candidate file's reader refuses a date that is not a full ISO date (resume/candidate.py:106), so a year alone needs a rule of its own.
  • What a Europass file says about them is in #692's table. Memberships have two elements in the current Candidate format: NetworksAndMemberships/NetworkAndMembership (an activity with Title, Date as a start, an end and an Ongoing flag, Description, Link, Location) and OrganizationAffiliations/OrganizationAffiliation (OrganizationName, whose validFrom and validTo attributes are the period, with Location and Link). Honours have no element; HonourAwardDate exists only as a child of the activity type. The plugin reads neither (src/postulo/plugins/europass/candidate.py:136-147); a file's memberships are named in its “nowhere to go” notice, and an honour typed into Others arrives as a Project (tests/test_europass_candidate.py:550-577).
  • The two are sensitive in different ways. The schema documentation describes NetworksAndMemberships as “memberships/involvement in professional associations, societies, political parties, sports clubs, etc.” and OrganizationAffiliations as including “membership in a union”. Article 9(1) of the GDPR lists “political opinions, religious or philosophical beliefs, or trade union membership” as special categories of personal data (text as reproduced at gdpr-info.eu).
  • No career section has an API, a processing-record line or a print choice (#308 is about the details of Your details).

Checked against main at f09b6010b.

Proposal

Two commits, honours first, each complete against the checklist in #692.

  1. Honour (“Honour or award”; heading “Honours and awards”). A ResumeItem with title (required, 200), awarded_by (200, optional), awarded_on (a date, optional), summary (a line or two, optional) and url (optional). Section slug honour, placed in OVERVIEW_ORDER where #692 puts it. A CV prints “title, awarded by” and the year, as it prints a certification, in both base themes, the plain text and the Word file. A new entry is placed by awarded_on (ordering.DATE_FIELDS).
  2. Membership (heading “Memberships”). organisation (required, 200), role (optional, 200, labelled “your role” as a project's is), start_date and end_date (both optional; an empty end with a start means “still”), summary and url. Slug membership. A CV prints “role, organisation” and “since 2015” or “2015–2018” (the years only, as a certification's date is), with the summary under it when there is one. A new entry is placed by start_date.
  3. Translations. An honour translates its summary and nothing else: a prize's name and the body that gave it are theirs, by the rule that keeps Certification untranslatable (resume/translatable.py:16-26). A membership translates role and summary, not organisation, as an experience does.
  4. Archive and candidate file. Blocks honours and memberships in RESUME_FIELDS, RESUME_MODELS and TRANSLATION_SECTIONS (src/postulo/core/export.py:153-162, :343-415); the archive importer's section_models and date_fields (core/importer.py:532-541) and its CV-entry map (:905-912); Kinds in resume/candidate.py that say what makes two entries the same one: an honour is its title, its giver and its date, a membership its organisation, its role and its dates (ongoing=True). Both formats go up by one from whatever they are when each commit lands.
  5. Europass. The Candidate's NetworkAndMembership and OrganizationAffiliation are read into Record as memberships: Title or OrganizationName becomes the organisation as written (a title like “Member of the University's Film-Making Society”, which is Europass's own example, is not split into a role and a body, because that would be a guess), Description the summary, Date or the name's validFrom and validTo the dates, Link the address. Nothing is read as an honour. The review page lists them before anything is saved, as it lists every row.
  6. Search finds both by name, organisation, giver and role, linking to #section-honour and #section-membership. Demo data gains one of each.
  7. Wiki and threat model. Your career record: the two sections and the rule for choosing between an honour, a certification and a grade. docs/THREAT-MODEL.md: a membership can reveal what Article 9(1) lists, so it has the same standing as the home address there: owned by one account, never in a list, a log or an error message, and printed only on a CV the person put it on.
  8. Help text on the membership form says, once, that a membership of a union, a party or a congregation says something about the person and is theirs to leave off any CV; nothing here asks for one.
  9. After #683, the company link. Each section gains a nullable company (SET_NULL) beside its text, offered and added as g1 does it (a company the form creates is marked from the career and given no industry, as #686's issuers are; only #685's education form assigns one), with a file never creating a company, a company merge moving the links to the kept company and the company's delete dialog counting them (jobs/merging.py, jobs/views.py:574-610: a SET_NULL link is not seen by the merge's own check, so it is cleared unless moved); both formats go up by one again, each with its line.

Decisions

These are recommendations the maintainer has not confirmed.

  • Two sections, one shape or two. Options: (a) two models with their own fields; (b) an abstract base with the shared organisation, url and summary; (c) one model with a type of “honour” or “membership”. Recommended: (a). An honour has one date and a title; a membership has a period and a role; the shared part is three fields, and a base for three fields is more indirection than copying them. (c) is the “one table with a kind” #692 turns down.
  • A body: text or a company. An association or an awarding body is not an employer. The note asks for companies where it names experience, learning and certification and says nothing of these; #683 and #686 expect honours and memberships to follow. Recommended: the rule in #692. Text first, as Certification.issuer is today, so nothing here waits on g1; then, in a second commit once g1 has landed, g1's optional link to one of the owner's companies, the text staying the CV's wording. How g1's “from the career” marker applies: a company the career form creates for a club or a prize body carries the marker, so it is left out of the posting, contact and agency pickers, the companies table and the map until a posting, a contact or an application is attached; being in the career never makes it somewhere to apply. #671's registry gets a role “membership organisation” (NACE division 94, “Activities of membership organisations”, section T in src/postulo/jobs/data/nace-2.1.json), offered first and never required, since an association named for a sport or a trade may sit in another division. For honours, #686's “awarding body” role orders the list the same way and restricts nothing: universities, foundations, firms and governments all award things.
  • Honour, certification or grade. Recommended rule, said in the form's help and the wiki: a certification says you met a standard and may lapse (it has an expiry and a credential to check); an honour is recognition, with neither; a degree's classification is the education entry's grade. A scholarship or a “best paper” is an honour.
  • A date, or a year. Options: a date field printed as a year, as a certification's is; the reduced ISO text #679 and #687 settle on, so “2019” is storable as itself. Recommended: a date field printed as a year, with the form saying only the year prints. It matches the one comparable section, and the candidate reader already handles it. If the maintainer prefers the one shared partial-date rule, it is a change of column type and its validator, and the format bump covers it.
  • A membership number. Recommended: not stored. It identifies the person at another body, nobody asked for it, and Europass's two elements have no place for it.
  • Where Europass puts an honour. The Candidate format has no element for one, and reading an activity that carries a HonourAwardDate as an honour is an inference nobody has checked against a real file (#318 is the open check). Recommended: read none for now, and add the reading if a real download shows the portal using it.
  • Special-category data. The sections do not need to be treated differently by the code: the data is the person's own, in their own account, covered by the archive and by deletion. Recommended: the wording and the threat-model line above, no field-level encryption, and the same “printed only where chosen” rule every career entry already has. The maintainer should confirm this reading of Article 9 for any instance that must answer to a regulator; nothing here depends on it.
  • Names. “Honours and awards” is Europass's heading and the maintainer's. Spelling follows the product's English (“Honours”). No pgettext context is needed for these two messages; “your role” already exists.

Prior art

No dependency. The standards are the Europass formats in #692's table (read in full for the elements named above; whether the portal fills them for a membership a person adds is not verified) and Article 9(1) of Regulation (EU) 2016/679, quoted as reproduced at gdpr-info.eu (the EUR-Lex text was not fetched).

Builds on

Issues #131 (career entries in other languages; free-text organisations), #181 (the candidate file), #203 (the order of entries), #244 and #318 (the Europass reader and its real-download check), #308 (what a CV prints). New issues of this set: #692 (shape and checklist), #683 (organisation as a company, “history only”), #671 (roles and divisions), #679 (the reduced-date rule).

Needed by

Nothing waits on it. #669's link from a document to the entry it proves may later offer an honour; it is not part of this issue.

Done when

  • A new tests/test_honours_and_memberships.py: each section's create, edit, delete and move through the registry; the CV prints “title, awarded by, year” and “role, organisation, since …” in the HTML themes, the plain text and the Word file, and a portfolio prints them without an empty heading; an empty end date means “since”; a new entry lands by its date; the form help carries the rules above.
  • tests/test_career_sections.py (the #692 guard) passes with both sections; tests/test_career_languages.py gains “an honour translates only its summary” and “a membership translates role and summary, not organisation” beside :83-86; tests/test_career_ordering.py covers the new DATE_FIELDS.
  • The standing checklist, for a new record type: the archive format goes up by one (core/export.py FORMAT_VERSION, 29 at f09b6010b; one more than whatever it is when each commit lands), tests/test_export.py round-trips both blocks and a CV that holds one entry of each; the candidate format (4) goes up by one with its line in SHAPES (tests/test_candidate_file.py:372-387), review rows for a good entry and a refused one, and tests/security/test_candidate_file.py for hostile or over-long values; tests/test_europass_candidate.py reads the two membership elements and reads no honour; the API is unchanged (there is none for the career); search finds both (tests/test_search.py); CV print choice (#308) and the processing record (core/gdpr.py) do not apply; tests/test_wiki_surface.py is untouched unless Record is changed on the surface, in which case its wiki line and Writing a plugin follow; tests/test_page_coverage.py needs no entry; tests/security/test_isolation_sweep.py and a per-section test show another account's entry is a 404; account deletion removes both (tests/test_account_deletion.py); strings are in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349).
  • After #683: tests/test_merging.py moves the links of both sections, tests/test_career_companies.py gains the honour and membership cases, and a file never creates a company.
  • The wiki pages and docs/THREAT-MODEL.md named above and the CHANGELOG line (ends (#N)) are in the same commits.

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

The maintainer's list, under Your career, names “Honours and awards” and “Memberships” among the additional sections. Postulo has a place for neither: a prize can only be typed into a project's summary and a membership into a skill group. This issue adds the two as sections of the career record, each a small typed entry, following the shape and the checklist settled in #692. ## What exists today - **Neither exists.** A search of `src/postulo` for honours, awards and membership finds no model, field or string for a career: the matches are the words in other senses (“Postulo honours robots.txt”, “membership of ISO 4217”). The career's eight kinds are in `src/postulo/resume/registry.py:28-80`. - **The nearest section is `Certification`**: a name, an issuer, an issue date, an optional expiry and a credential link (`src/postulo/resume/models.py:215-228`). Its issuer is free text, its name is not translatable, and a CV prints it as “name, issuer” and the year (`src/postulo/templates/documents/themes/base_cv.html:117-125`, `src/postulo/documents/rendering.py:363-371`). An honour reads the same way on a CV and differs in what it is: recognition, with nothing to check and no expiry. - **A degree's classification is already somewhere**: `Education.grade` is “as the institution words it” (`resume/forms.py:122`), so “First-class honours” and “cum laude” have a home that is not this section. - **Organisations are free text on purpose** (`Experience.organisation`, `resume/models.py:65-74`, #131; `resume/translatable.py:16-22`: “*Universidade de Lisboa* stays that on an English CV”), and #683 is reopening that for employers. The company list is the one an application's picker draws on. - **A new entry is placed by its date** where its section has one: `DATE_FIELDS` names the field for experience, education and certifications (`src/postulo/resume/ordering.py:28-32`; the candidate import's `_place` reads it too, `resume/candidate.py:1382`), and the candidate file's reader refuses a date that is not a full ISO date (`resume/candidate.py:106`), so a year alone needs a rule of its own. - **What a Europass file says about them** is in #692's table. Memberships have two elements in the current Candidate format: `NetworksAndMemberships/NetworkAndMembership` (an activity with `Title`, `Date` as a start, an end and an `Ongoing` flag, `Description`, `Link`, `Location`) and `OrganizationAffiliations/OrganizationAffiliation` (`OrganizationName`, whose `validFrom` and `validTo` attributes are the period, with `Location` and `Link`). Honours have no element; `HonourAwardDate` exists only as a child of the activity type. The plugin reads neither (`src/postulo/plugins/europass/candidate.py:136-147`); a file's memberships are named in its “nowhere to go” notice, and an honour typed into `Others` arrives as a Project (`tests/test_europass_candidate.py:550-577`). - **The two are sensitive in different ways.** The schema documentation describes `NetworksAndMemberships` as “memberships/involvement in professional associations, societies, political parties, sports clubs, etc.” and `OrganizationAffiliations` as including “membership in a union”. Article 9(1) of the GDPR lists “political opinions, religious or philosophical beliefs, or trade union membership” as special categories of personal data (text as reproduced at gdpr-info.eu). - No career section has an API, a processing-record line or a print choice (#308 is about the details of *Your details*). Checked against `main` at `f09b6010b`. ## Proposal Two commits, honours first, each complete against the checklist in #692. 1. **`Honour`** (“Honour or award”; heading “Honours and awards”). A `ResumeItem` with `title` (required, 200), `awarded_by` (200, optional), `awarded_on` (a date, optional), `summary` (a line or two, optional) and `url` (optional). Section slug `honour`, placed in `OVERVIEW_ORDER` where #692 puts it. A CV prints “title, awarded by” and the year, as it prints a certification, in both base themes, the plain text and the Word file. A new entry is placed by `awarded_on` (`ordering.DATE_FIELDS`). 2. **`Membership`** (heading “Memberships”). `organisation` (required, 200), `role` (optional, 200, labelled “your role” as a project's is), `start_date` and `end_date` (both optional; an empty end with a start means “still”), `summary` and `url`. Slug `membership`. A CV prints “role, organisation” and “since 2015” or “2015–2018” (the years only, as a certification's date is), with the summary under it when there is one. A new entry is placed by `start_date`. 3. **Translations.** An honour translates its `summary` and nothing else: a prize's name and the body that gave it are theirs, by the rule that keeps `Certification` untranslatable (`resume/translatable.py:16-26`). A membership translates `role` and `summary`, not `organisation`, as an experience does. 4. **Archive and candidate file.** Blocks `honours` and `memberships` in `RESUME_FIELDS`, `RESUME_MODELS` and `TRANSLATION_SECTIONS` (`src/postulo/core/export.py:153-162`, `:343-415`); the archive importer's `section_models` and `date_fields` (`core/importer.py:532-541`) and its CV-entry map (`:905-912`); `Kind`s in `resume/candidate.py` that say what makes two entries the same one: an honour is its title, its giver and its date, a membership its organisation, its role and its dates (`ongoing=True`). Both formats go up by one from whatever they are when each commit lands. 5. **Europass.** The Candidate's `NetworkAndMembership` and `OrganizationAffiliation` are read into `Record` as memberships: `Title` or `OrganizationName` becomes the organisation as written (a title like “Member of the University's Film-Making Society”, which is Europass's own example, is not split into a role and a body, because that would be a guess), `Description` the summary, `Date` or the name's `validFrom` and `validTo` the dates, `Link` the address. Nothing is read as an honour. The review page lists them before anything is saved, as it lists every row. 6. **Search** finds both by name, organisation, giver and role, linking to `#section-honour` and `#section-membership`. **Demo data** gains one of each. 7. **Wiki and threat model.** *Your career record*: the two sections and the rule for choosing between an honour, a certification and a grade. `docs/THREAT-MODEL.md`: a membership can reveal what Article 9(1) lists, so it has the same standing as the home address there: owned by one account, never in a list, a log or an error message, and printed only on a CV the person put it on. 8. **Help text** on the membership form says, once, that a membership of a union, a party or a congregation says something about the person and is theirs to leave off any CV; nothing here asks for one. 9. **After #683, the company link.** Each section gains a nullable `company` (`SET_NULL`) beside its text, offered and added as g1 does it (a company the form creates is marked from the career and given no industry, as #686's issuers are; only #685's education form assigns one), with a file never creating a company, a company merge moving the links to the kept company and the company's delete dialog counting them (`jobs/merging.py`, `jobs/views.py:574-610`: a `SET_NULL` link is not seen by the merge's own check, so it is cleared unless moved); both formats go up by one again, each with its line. ## Decisions These are recommendations the maintainer has not confirmed. - **Two sections, one shape or two.** Options: (a) two models with their own fields; (b) an abstract base with the shared `organisation`, `url` and `summary`; (c) one model with a type of “honour” or “membership”. **Recommended: (a).** An honour has one date and a title; a membership has a period and a role; the shared part is three fields, and a base for three fields is more indirection than copying them. (c) is the “one table with a kind” #692 turns down. - **A body: text or a company.** An association or an awarding body is not an employer. The note asks for companies where it names experience, learning and certification and says nothing of these; #683 and #686 expect honours and memberships to follow. **Recommended: the rule in #692.** Text first, as `Certification.issuer` is today, so nothing here waits on g1; then, in a second commit once g1 has landed, g1's optional link to one of the owner's companies, the text staying the CV's wording. How g1's “from the career” marker applies: a company the career form creates for a club or a prize body carries the marker, so it is left out of the posting, contact and agency pickers, the companies table and the map until a posting, a contact or an application is attached; being in the career never makes it somewhere to apply. #671's registry gets a role “membership organisation” (NACE division 94, “Activities of membership organisations”, section T in `src/postulo/jobs/data/nace-2.1.json`), offered first and never required, since an association named for a sport or a trade may sit in another division. For honours, #686's “awarding body” role orders the list the same way and restricts nothing: universities, foundations, firms and governments all award things. - **Honour, certification or grade.** **Recommended rule, said in the form's help and the wiki:** a *certification* says you met a standard and may lapse (it has an expiry and a credential to check); an *honour* is recognition, with neither; a degree's *classification* is the education entry's grade. A scholarship or a “best paper” is an honour. - **A date, or a year.** Options: a date field printed as a year, as a certification's is; the reduced ISO text #679 and #687 settle on, so “2019” is storable as itself. **Recommended: a date field printed as a year, with the form saying only the year prints.** It matches the one comparable section, and the candidate reader already handles it. If the maintainer prefers the one shared partial-date rule, it is a change of column type and its validator, and the format bump covers it. - **A membership number.** **Recommended: not stored.** It identifies the person at another body, nobody asked for it, and Europass's two elements have no place for it. - **Where Europass puts an honour.** The Candidate format has no element for one, and reading an activity that carries a `HonourAwardDate` as an honour is an inference nobody has checked against a real file (#318 is the open check). **Recommended: read none for now**, and add the reading if a real download shows the portal using it. - **Special-category data.** The sections do not need to be treated differently by the code: the data is the person's own, in their own account, covered by the archive and by deletion. **Recommended:** the wording and the threat-model line above, no field-level encryption, and the same “printed only where chosen” rule every career entry already has. The maintainer should confirm this reading of Article 9 for any instance that must answer to a regulator; nothing here depends on it. - **Names.** “Honours and awards” is Europass's heading and the maintainer's. Spelling follows the product's English (“Honours”). No `pgettext` context is needed for these two messages; “your role” already exists. ## Prior art No dependency. The standards are the Europass formats in #692's table (read in full for the elements named above; whether the portal fills them for a membership a person adds is not verified) and Article 9(1) of Regulation (EU) 2016/679, quoted as reproduced at gdpr-info.eu (the EUR-Lex text was not fetched). ## Builds on Issues #131 (career entries in other languages; free-text organisations), #181 (the candidate file), #203 (the order of entries), #244 and #318 (the Europass reader and its real-download check), #308 (what a CV prints). New issues of this set: #692 (shape and checklist), #683 (organisation as a company, “history only”), #671 (roles and divisions), #679 (the reduced-date rule). ## Needed by Nothing waits on it. #669's link from a document to the entry it proves may later offer an honour; it is not part of this issue. ## Done when - A new `tests/test_honours_and_memberships.py`: each section's create, edit, delete and move through the registry; the CV prints “title, awarded by, year” and “role, organisation, since …” in the HTML themes, the plain text and the Word file, and a portfolio prints them without an empty heading; an empty end date means “since”; a new entry lands by its date; the form help carries the rules above. - `tests/test_career_sections.py` (the #692 guard) passes with both sections; `tests/test_career_languages.py` gains “an honour translates only its summary” and “a membership translates role and summary, not organisation” beside `:83-86`; `tests/test_career_ordering.py` covers the new `DATE_FIELDS`. - The standing checklist, for a new record type: the archive format goes up by one (`core/export.py` `FORMAT_VERSION`, 29 at `f09b6010b`; one more than whatever it is when each commit lands), `tests/test_export.py` round-trips both blocks and a CV that holds one entry of each; the candidate format (4) goes up by one with its line in `SHAPES` (`tests/test_candidate_file.py:372-387`), review rows for a good entry and a refused one, and `tests/security/test_candidate_file.py` for hostile or over-long values; `tests/test_europass_candidate.py` reads the two membership elements and reads no honour; the API is unchanged (there is none for the career); search finds both (`tests/test_search.py`); CV print choice (#308) and the processing record (`core/gdpr.py`) do not apply; `tests/test_wiki_surface.py` is untouched unless `Record` is changed on the surface, in which case its wiki line and `Writing a plugin` follow; `tests/test_page_coverage.py` needs no entry; `tests/security/test_isolation_sweep.py` and a per-section test show another account's entry is a 404; account deletion removes both (`tests/test_account_deletion.py`); strings are in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349). - After #683: `tests/test_merging.py` moves the links of both sections, `tests/test_career_companies.py` gains the honour and membership cases, and a file never creates a company. - The wiki pages and `docs/THREAT-MODEL.md` named above and the CHANGELOG line (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:05 +00:00
Author
Owner

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

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