An education entry names its institution as a company, one that is in education #685

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

The maintainer's list says, under Your career: “Learning institution are Companies (with valid NACE code).” Today the institution of an education entry is free text, on purpose (#131), and a company has no NACE code of its own. This issue links the entry to a company the way #683 does for an experience, and says what “valid NACE code” can mean given what Postulo stores.

What exists today

  • The institution is free text. Education.institution is a required CharField(200) (src/postulo/resume/models.py:104). It is not translatable on purpose, for the reason #131 gave about Universidade de Lisboa (src/postulo/resume/translatable.py:16-27; pinned by tests/test_career_languages.py:72-79, :305-312). The wiki repeats it (Your-career-record.md, “Some things are not translated, on purpose”). It is read by the form (resume/forms.py:98-124), the CV themes (templates/documents/themes/base_cv.html:137, base_portfolio.html:142), the preview, the text and the Word file (documents/rendering.py:310, :379), search (core/search.py:476-481), the candidate file, where an entry is its institution, qualification and dates (resume/candidate.py:198-217), and the Europass import (resume/importing.py:240-250).
  • How a career entry can point at a company, and what that costs, is set out in #683: the archive restores the career before the companies (core/importer.py:530-575, :597), the archive and the candidate file share one block and one fingerprint (core/export.py:355-366, tests/test_candidate_file.py:354-387), a company merge and delete know nothing of the career (jobs/merging.py:230-250, jobs/views.py:574-610), and every list of companies would gain the school.
  • A company has no NACE code. Only Industry has one: Industry.code is the division, set from the name by save() when the name is a division's name in the reader's language or English, and a company has industries through a many-to-many (jobs/models.py:42-81, :288-290; jobs/industries.py:165-176). The vendored file holds the 22 sections and 87 divisions of NACE Rev. 2.1 in 24 languages and no groups or classes (jobs/data/nace-2.1.json, data/LICENCE.md: “Groups (three digits) and classes (four) were not taken”). #671 adds the registry of which divisions qualify a company for a role.
  • Education is division 85, and in Rev. 2.1 it sits in section Q. The vendored file says so ("85": {"section": "Q"}), and Eurostat's NACE Rev. 2.1, 2025 edition (KS-GQ-24-007-EN-N) heads it “SECTION Q -- EDUCATION”; Rev. 2 had education in section P, which is where an older note or table may say it still is. The division's classes there: 85.10 pre-primary, 85.20 primary, 85.31 general secondary, 85.32 vocational secondary, 85.33 post-secondary non-tertiary, 85.40 tertiary education (“public and private universities”, the granting of degrees “at short-cycle, bachelor's or equivalent level, master's or equivalent level, and doctoral level”), 85.51 sports and recreation, 85.52 cultural, 85.53 driving schools, 85.59 other education n.e.c. (examination review courses, language and computer training), 85.61 intermediation for courses and tutors and 85.69 educational support n.e.c. Non-formal courses and training are group 85.5 by the classification's own definition. Every one of these codes and titles was read in Eurostat's publication.
  • jobs/industries.py still describes Rev. 2. Its docstring speaks of “21 sections” and “615 classes”; Rev. 2.1 has 22 sections, 87 divisions, 287 groups and 651 classes. Not part of this issue's fix, but it is wrong in the file the issue touches.
  • A school is an “employer” to the code. CompanyKind has only employer and employment service (jobs/models.py:209-221); nothing counts employers today except the industry tally of applications, where a school with no applications never appears (applications/reports.py:522-529).

Checked against main at f09b6010b.

Proposal

Three commits, after #683 (the link, the marker, the files) and #671 (the registry); each leaves main green.

  1. The link. Education.company, a nullable foreign key to jobs.Company, SET_NULL, with institution kept as the required text every theme and file reads. The form's institution box offers the person's companies as #683's does, in the order e1's registry gives for the role “place of learning”: the companies whose industries include division 85 first, then every other company below them (a typed box hides nothing). On save the text is matched to the owner's company by name, case-insensitively, or a company is added, marked as from the career. A company added from an education entry is given the industry named by division 85 in the person's language, visibly (the form says “Added to your companies as Education”), and the person can remove it; an existing company is never given an industry by this form.
  2. The notice. Where the chosen company has no industry in division 85, the entry says so in one line beside the institution (“None of this company's industries is Education, so it is not listed as a school or university”), with a link to the company to add one. It never blocks saving.
  3. Files and history. The archive's education block and the candidate file gain company (name or blank), read as #683 reads it: by name, after the companies exist; offered as a link on the candidate review page only on an exact match; a file never creates a company; the Europass import links by exact name and adds none. A migration links an entry where exactly one company of the same owner has the institution's name, and adds none. The company's page lists the education entries that name it with the experiences, the merge moves them, and the delete dialog counts them.

Decisions

  • What “valid NACE code” means. The list's words, read two ways: (a) the company carries a code that is a real Rev. 2.1 code, any; (b) its code is one that fits a place of learning. Recommended: (b), at division level (85 whole). (a) says nothing about a school; (b) is what a code is for, and 85 is the one division where the classification itself puts universities (85.40), schools (85.10 to 85.33) and non-formal training (85.5). Classes are not needed, so nothing is harvested and the vendored file stays as it is. A research institute (72.10, 72.20) that a person names as the place of a doctorate is not in 85; Recommended: leave 72 out, and say so in the registry's comment: the role's set is data and one line adds it.
  • Strict or suggested. Strict refuses a company outside division 85 on the page and in a file; suggested orders and warns. Recommended: suggested, for #671's reason: no company carries an industry coded 85 today, the migration cannot give one, and a strict rule would refuse every entry already written. A stricter mode can be chosen once companies come with a code from somewhere other than typing.
  • Who gives a new company its industry. Classifying a business is the person's act; but a company added from the education form is, by the person's own statement, a place of learning. Recommended: assign division 85 only to a company this form just created, visibly, removable, and never to an existing one (an alternative: assign nothing and show the notice; that leaves every new school without its code until somebody remembers).
  • What a CV prints. The entry's own text, unchanged, as in #683. The maintainer's own example of a name in two languages (“Universidade de Aveiro” is “University of Aveiro” in English) is an institution, and contradicts #131's Universidade de Lisboa. Recommended: the same answer as #683: no change to any CV here; when #673 gives a company its names, the one per-CV switch “Print organisations in the CV's language” covers experience and education alike, off by default.
  • Education that has no institution. The text stays required, so self-taught study and a course with no school are still written as text and are never forced into a company.
  • No new company kind. A school stays an employer in the code; a kind says what a company is to the search, and that is what the marker from #683 is for.

Prior art

No new dependency. NACE Rev. 2.1 is Eurostat's, published by the Publications Office and already vendored under CC BY 4.0 (jobs/data/LICENCE.md); the rule that a classification is taken and not invented is #140's and #266's, and the maintainer's list states it again. Source for every code above: Eurostat, NACE Rev. 2.1 — Statistical classification of economic activities in the European Union, 2025 edition (doi:10.2785/155339), read in full for division 85 and checked, class by class, against the Publications Office copy a previous attempt had harvested (1,020 of the 1,025 divisions, groups and classes carry the same title; the five that differ are in divisions 11, 20, 22, 47 and 77, by a word, an apostrophe or a line break).

Builds on

Issues #131 (career entries per language, reversed in part), #140 and #266 (a classification is taken, not written), #202 (company kinds), #683, #671.

Needed by

#695 (the provider of a course), #673 (a name in a language, from Wikidata).

Done when

  • tests/test_career_companies.py (new in #683) gains the education cases: saving links or adds a company marked as from the career, gives a newly added company the division-85 industry and no existing one, never uses another owner's company; the migration links exact matches only.
  • #671's registry test (a new file beside tests/test_nace.py, which is about the classification file and industries): the “place of learning” role is division 85 and nothing else, an unknown division is refused, and every code named in the registry exists in the vendored file; a company with the industry “Education” (and its French and Portuguese names) qualifies, one without does not.
  • The form test: qualifying companies come first and the rest follow; the notice shows for a non-qualifying company and does not block; tests/test_help_text.py follows the field.
  • tests/test_merging.py moves education entries; tests/test_jobs_isolation.py and tests/security/test_isolation_sweep.py show another account's company is neither offered nor linked; tests/test_career_languages.py still passes unchanged.
  • tests/test_export.py and the archive round trip carry company on an education entry at the new format; tests/test_candidate_file.py holds the new SHAPES line (the education block changes with #684 and this issue, and each commit bumps the format it changes) and a hostile company in tests/security/test_candidate_file.py; tests/test_importers.py and tests/test_europass_candidate.py for the links.
  • No API, no search change, no CV print choice (nothing new is printed), no line in the processing record (core/gdpr.py). Standing checklist otherwise: archive format one more than whatever it is when the commit lands (29 at f09b6010b), candidate format likewise (4), strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349); Your-career-record.md and jobs/industries.py's docstring (22 sections, 651 classes) changed in the same commits.

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

The maintainer's list says, under Your career: “Learning institution are Companies (with valid NACE code).” Today the institution of an education entry is free text, on purpose (#131), and a company has no NACE code of its own. This issue links the entry to a company the way #683 does for an experience, and says what “valid NACE code” can mean given what Postulo stores. ## What exists today - **The institution is free text.** `Education.institution` is a required `CharField(200)` (`src/postulo/resume/models.py:104`). It is not translatable on purpose, for the reason #131 gave about *Universidade de Lisboa* (`src/postulo/resume/translatable.py:16-27`; pinned by `tests/test_career_languages.py:72-79`, `:305-312`). The wiki repeats it (`Your-career-record.md`, “Some things are not translated, on purpose”). It is read by the form (`resume/forms.py:98-124`), the CV themes (`templates/documents/themes/base_cv.html:137`, `base_portfolio.html:142`), the preview, the text and the Word file (`documents/rendering.py:310`, `:379`), search (`core/search.py:476-481`), the candidate file, where an entry is its institution, qualification and dates (`resume/candidate.py:198-217`), and the Europass import (`resume/importing.py:240-250`). - **How a career entry can point at a company, and what that costs**, is set out in #683: the archive restores the career before the companies (`core/importer.py:530-575`, `:597`), the archive and the candidate file share one block and one fingerprint (`core/export.py:355-366`, `tests/test_candidate_file.py:354-387`), a company merge and delete know nothing of the career (`jobs/merging.py:230-250`, `jobs/views.py:574-610`), and every list of companies would gain the school. - **A company has no NACE code.** Only `Industry` has one: `Industry.code` is the division, set from the name by `save()` when the name is a division's name in the reader's language or English, and a company has industries through a many-to-many (`jobs/models.py:42-81`, `:288-290`; `jobs/industries.py:165-176`). The vendored file holds the **22 sections and 87 divisions** of NACE Rev. 2.1 in 24 languages and no groups or classes (`jobs/data/nace-2.1.json`, `data/LICENCE.md`: “Groups (three digits) and classes (four) were not taken”). #671 adds the registry of which divisions qualify a company for a role. - **Education is division 85, and in Rev. 2.1 it sits in section Q.** The vendored file says so (`"85": {"section": "Q"}`), and Eurostat's *NACE Rev. 2.1, 2025 edition* (KS-GQ-24-007-EN-N) heads it “SECTION Q -- EDUCATION”; Rev. 2 had education in section P, which is where an older note or table may say it still is. The division's classes there: 85.10 pre-primary, 85.20 primary, 85.31 general secondary, 85.32 vocational secondary, 85.33 post-secondary non-tertiary, **85.40 tertiary education** (“public and private universities”, the granting of degrees “at short-cycle, bachelor's or equivalent level, master's or equivalent level, and doctoral level”), 85.51 sports and recreation, 85.52 cultural, 85.53 driving schools, 85.59 other education n.e.c. (examination review courses, language and computer training), 85.61 intermediation for courses and tutors and 85.69 educational support n.e.c. Non-formal courses and training are group 85.5 by the classification's own definition. Every one of these codes and titles was read in Eurostat's publication. - **`jobs/industries.py` still describes Rev. 2.** Its docstring speaks of “21 sections” and “615 classes”; Rev. 2.1 has 22 sections, 87 divisions, 287 groups and 651 classes. Not part of this issue's fix, but it is wrong in the file the issue touches. - **A school is an “employer” to the code.** `CompanyKind` has only *employer* and *employment service* (`jobs/models.py:209-221`); nothing counts employers today except the industry tally of applications, where a school with no applications never appears (`applications/reports.py:522-529`). Checked against `main` at `f09b6010b`. ## Proposal Three commits, after #683 (the link, the marker, the files) and #671 (the registry); each leaves `main` green. 1. **The link.** `Education.company`, a nullable foreign key to `jobs.Company`, `SET_NULL`, with `institution` kept as the required text every theme and file reads. The form's institution box offers the person's companies as #683's does, **in the order e1's registry gives for the role “place of learning”**: the companies whose industries include division 85 first, then every other company below them (a typed box hides nothing). On save the text is matched to the owner's company by name, case-insensitively, or a company is added, marked as from the career. A company added from an education entry is given the industry named by division 85 in the person's language, visibly (the form says “Added to your companies as Education”), and the person can remove it; an existing company is never given an industry by this form. 2. **The notice.** Where the chosen company has no industry in division 85, the entry says so in one line beside the institution (“None of this company's industries is Education, so it is not listed as a school or university”), with a link to the company to add one. It never blocks saving. 3. **Files and history.** The archive's education block and the candidate file gain `company` (name or blank), read as #683 reads it: by name, after the companies exist; offered as a link on the candidate review page only on an exact match; **a file never creates a company**; the Europass import links by exact name and adds none. A migration links an entry where exactly one company of the same owner has the institution's name, and adds none. The company's page lists the education entries that name it with the experiences, the merge moves them, and the delete dialog counts them. ## Decisions - **What “valid NACE code” means.** The list's words, read two ways: (a) the company carries a code that is a real Rev. 2.1 code, any; (b) its code is one that fits a place of learning. **Recommended: (b), at division level (85 whole).** (a) says nothing about a school; (b) is what a code is for, and 85 is the one division where the classification itself puts universities (85.40), schools (85.10 to 85.33) and non-formal training (85.5). Classes are not needed, so nothing is harvested and the vendored file stays as it is. A research institute (72.10, 72.20) that a person names as the place of a doctorate is not in 85; **Recommended:** leave 72 out, and say so in the registry's comment: the role's set is data and one line adds it. - **Strict or suggested.** Strict refuses a company outside division 85 on the page and in a file; suggested orders and warns. **Recommended: suggested**, for #671's reason: no company carries an industry coded 85 today, the migration cannot give one, and a strict rule would refuse every entry already written. A stricter mode can be chosen once companies come with a code from somewhere other than typing. - **Who gives a new company its industry.** Classifying a business is the person's act; but a company added from the education form is, by the person's own statement, a place of learning. **Recommended:** assign division 85 only to a company this form just created, visibly, removable, and never to an existing one (an alternative: assign nothing and show the notice; that leaves every new school without its code until somebody remembers). - **What a CV prints.** The entry's own text, unchanged, as in #683. The maintainer's own example of a name in two languages (“Universidade de Aveiro” is “University of Aveiro” in English) is an institution, and contradicts #131's *Universidade de Lisboa*. **Recommended:** the same answer as #683: no change to any CV here; when #673 gives a company its names, the one per-CV switch “Print organisations in the CV's language” covers experience and education alike, off by default. - **Education that has no institution.** The text stays required, so self-taught study and a course with no school are still written as text and are never forced into a company. - **No new company kind.** A school stays an *employer* in the code; a kind says what a company is to the search, and that is what the marker from #683 is for. ## Prior art No new dependency. NACE Rev. 2.1 is Eurostat's, published by the Publications Office and already vendored under CC BY 4.0 (`jobs/data/LICENCE.md`); the rule that a classification is taken and not invented is #140's and #266's, and the maintainer's list states it again. Source for every code above: Eurostat, *NACE Rev. 2.1 — Statistical classification of economic activities in the European Union*, 2025 edition (doi:10.2785/155339), read in full for division 85 and checked, class by class, against the Publications Office copy a previous attempt had harvested (1,020 of the 1,025 divisions, groups and classes carry the same title; the five that differ are in divisions 11, 20, 22, 47 and 77, by a word, an apostrophe or a line break). ## Builds on Issues #131 (career entries per language, reversed in part), #140 and #266 (a classification is taken, not written), #202 (company kinds), #683, #671. ## Needed by #695 (the provider of a course), #673 (a name in a language, from Wikidata). ## Done when - `tests/test_career_companies.py` (new in #683) gains the education cases: saving links or adds a company marked as from the career, gives a newly added company the division-85 industry and no existing one, never uses another owner's company; the migration links exact matches only. - #671's registry test (a new file beside `tests/test_nace.py`, which is about the classification file and industries): the “place of learning” role is division 85 and nothing else, an unknown division is refused, and every code named in the registry exists in the vendored file; a company with the industry “Education” (and its French and Portuguese names) qualifies, one without does not. - The form test: qualifying companies come first and the rest follow; the notice shows for a non-qualifying company and does not block; `tests/test_help_text.py` follows the field. - `tests/test_merging.py` moves education entries; `tests/test_jobs_isolation.py` and `tests/security/test_isolation_sweep.py` show another account's company is neither offered nor linked; `tests/test_career_languages.py` still passes unchanged. - `tests/test_export.py` and the archive round trip carry `company` on an education entry at the new format; `tests/test_candidate_file.py` holds the new `SHAPES` line (the education block changes with #684 and this issue, and each commit bumps the format it changes) and a hostile `company` in `tests/security/test_candidate_file.py`; `tests/test_importers.py` and `tests/test_europass_candidate.py` for the links. - No API, no search change, no CV print choice (nothing new is printed), no line in the processing record (`core/gdpr.py`). Standing checklist otherwise: archive format one more than whatever it is when the commit lands (29 at `f09b6010b`), candidate format likewise (4), strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349); `Your-career-record.md` and `jobs/industries.py`'s docstring (22 sections, 651 classes) changed 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:00 +00:00
Author
Owner

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

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