A certification names its issuer as a company #686

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

The maintainer's list says, under Your career: “Certification is issued by a Company (with valid NACE code).” Today the issuer of a certification is a line of free text. Unlike a school or an employer, the organisations that issue certifications do not sit in one NACE division, so the part of the sentence that needs care is “valid NACE code”.

What exists today

  • The issuer is optional free text. Certification.issuer is a CharField(200, blank=True), helped by “Who awarded it.” (src/postulo/resume/models.py:215-228, resume/forms.py:192-201). A certification translates nothing, on purpose: “both of its texts are the awarding body's wording — the name of a credential is the credential” (resume/translatable.py:18-26; tests/test_career_languages.py:83-85 and the screen at :342-351). #131 (closed) decided that.
  • Where the text is read. The CV prints “name, issuer” (templates/documents/themes/base_cv.html:121, base_portfolio.html:125, documents/rendering.py:363-371), search covers both (core/search.py:489), and a certification is “its name and its issuer” and its dates in the candidate file (resume/candidate.py:259-266). Seed data names CNCF (core/management/commands/seed_demo.py:476).
  • Everything a career entry that points at a company touches is listed in #683: the archive restores the career before the companies (core/importer.py:530-575, :597), the career block is shared by the archive and the candidate file and fingerprinted (core/export.py:382-391, 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 issuer.
  • A company has no NACE code of its own; Industry.code is a NACE division set from the name, 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, 177 KB; data/LICENCE.md: “Groups (three digits) and classes (four) were not taken”, which was a choice and not a licence term). Industry.code is max_length=4 (jobs/models.py:61): it holds 85 and would hold 8569, not 85.69.
  • What the classification says about who certifies people (Eurostat, NACE Rev. 2.1, 2025 edition, KS-GQ-24-007-EN-N, explanatory notes; every code and title below was read there):
    • 85.59 Other education n.e.c. includes “professional examination review courses” and “computer training (e.g. general computer use, programming courses, use of computer applications)”.
    • 85.69 Educational support activities n.e.c. includes “educational testing evaluation activities” and “examination and testing of regulated professions (e.g. pilots)”.
    • 85.32 Vocational secondary education covers “education activities of driving schools for occupational drivers” and schools for professional pilots; 85.53 Driving school activities covers those “not issuing commercial certificates and permits”.
    • 94.12 Activities of professional membership organisations includes “the establishment and supervision of standards of practice”.
    • 71.20 Technical testing and analysis includes “certification of products”: products, not people, so it is not an awarding body in this sense.
    • A vendor that certifies its own users is classified by what it sells (hosting is 63.10 and programming is 62.10, both read), and a foundation or a company that trains is classified by that business too. Which class Eurostat gives a cloud vendor or a foundation such as the CNCF is not verified.
    • Education, 85, is section Q in Rev. 2.1 (section P in Rev. 2).

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. Certification.company, a nullable foreign key to jobs.Company, SET_NULL, with issuer kept as the optional text every theme and file reads. The issuer box offers the person's companies as #683's does, in the order e1's registry gives for the role “awarding body”: the companies whose industries include division 85 or 94 first, then every other company below them. 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, with no industry assigned: a school can be classified from the form it was typed into, a vendor or a professional body cannot.
  2. The notice. Where the chosen company has no industry with a NACE code at all, the entry says so in one line (“This company has none of its industries from the NACE list yet”) with a link to the company, which is the reading of “valid NACE code” recommended below. A company with a code outside 85 and 94 gets no notice. It never blocks saving.
  3. Files and history, as #685: company (name or blank) in the archive and the candidate file, linked by name after the companies exist, offered as a link on the review page only on an exact match, never created from a file; the Europass import adds none; a migration links exact name matches only; the company page lists the certifications that name it; the merge moves them and the delete dialog counts them.

Decisions

  • What “valid NACE code” means. (a) The company carries some valid Rev. 2.1 code, and the form says when it carries none; (b) its code is in a defined set of awarding bodies, and nothing else is accepted; (c) a set per kind of certification. Recommended: (a), with the set {85, 94} used only to put likely issuers first. The notes above show that people's certificates come from education and testing bodies (85), professional bodies (94), vendors in any division and public authorities (84, whose notes were not read for this), and a strict set would turn away the issuers most IT and trade certificates have. If the maintainer wants (b), the set is data in e1's registry and an issuer outside it has to be classified by hand first.
  • Division or class. Class-level precision (85.69 or 94.12) would need the 651 classes of Rev. 2.1 harvested from the Publications Office copy the vendored file came from (same licence, CC BY 4.0), a spelling for a class in Industry.code, and a larger file: the English-only titles of the 1,025 divisions, groups and classes come to 65 KB, against 177 KB for 109 entries in 24 languages today, so the full set in 24 languages would be well over a megabyte (an estimate, not measured). Recommended: division level only, as #671 does; nothing here needs a class.
  • What a CV prints. The issuer's own text, unchanged. A certification is the awarding body's wording and #131 kept it out of every translation; #673's per-CV switch for companies' names in the CV's language (see #683) should leave certifications out. Recommended: both.
  • Optional stays optional. A certification with no issuer is still valid, and one whose issuer is a person (a trainer) is text. Nothing forces a company.
  • Where a credential's issuer is a body Postulo already knows. #670 reads an awarding body from a European Digital Credential; once this exists it offers the exact-name link, and never adds a company from a file.

Prior art

No new dependency. The classification is Eurostat's NACE Rev. 2.1, 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. The classes quoted are from Eurostat, NACE Rev. 2.1 — Statistical classification of economic activities in the European Union, 2025 edition (doi:10.2785/155339). Not verified: how Eurostat's notes treat public authorities that issue licences (division 84), and which class a cloud vendor or a foundation falls in.

Builds on

Issues #131 (a certification translates nothing), #140 and #266, #683, #671.

Needed by

#670 (the issuer of a credential), #693 (awards and memberships name an organisation as a company too).

Done when

  • tests/test_career_companies.py (new in #683) gains the certification cases: saving links or adds a company marked as from the career, never assigns an industry, never uses another owner's company; the migration links exact matches only; an entry with no issuer saves as before.
  • #671's registry test (a new file beside tests/test_nace.py): the “awarding body” role is divisions 85 and 94, every code in the registry exists in the vendored file; the order puts a company with “Education” or a membership-organisation industry first and loses none; the notice shows for a company with no coded industry, not for one with an industry outside 85 and 94, and never blocks.
  • tests/test_career_languages.py still passes unchanged (a certification translates nothing, :83-85, :342-351); tests/test_help_text.py follows the field.
  • tests/test_merging.py moves certifications; tests/test_jobs_isolation.py and tests/security/test_isolation_sweep.py show another account's company is neither offered nor linked.
  • tests/test_export.py and the archive round trip carry company on a certification at the new format; tests/test_candidate_file.py holds the new SHAPES line and tests/security/test_candidate_file.py a hostile company; tests/test_importers.py for the importer's pass.
  • 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: “Certification is issued by a Company (with valid NACE code).” Today the issuer of a certification is a line of free text. Unlike a school or an employer, the organisations that issue certifications do not sit in one NACE division, so the part of the sentence that needs care is “valid NACE code”. ## What exists today - **The issuer is optional free text.** `Certification.issuer` is a `CharField(200, blank=True)`, helped by “Who awarded it.” (`src/postulo/resume/models.py:215-228`, `resume/forms.py:192-201`). A certification translates nothing, on purpose: “both of its texts are the awarding body's wording — the name of a credential *is* the credential” (`resume/translatable.py:18-26`; `tests/test_career_languages.py:83-85` and the screen at `:342-351`). #131 (closed) decided that. - **Where the text is read.** The CV prints “name, issuer” (`templates/documents/themes/base_cv.html:121`, `base_portfolio.html:125`, `documents/rendering.py:363-371`), search covers both (`core/search.py:489`), and a certification is “its name and its issuer” and its dates in the candidate file (`resume/candidate.py:259-266`). Seed data names CNCF (`core/management/commands/seed_demo.py:476`). - **Everything a career entry that points at a company touches** is listed in #683: the archive restores the career before the companies (`core/importer.py:530-575`, `:597`), the career block is shared by the archive and the candidate file and fingerprinted (`core/export.py:382-391`, `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 issuer. - **A company has no NACE code of its own**; `Industry.code` is a NACE **division** set from the name, 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`, 177 KB; `data/LICENCE.md`: “Groups (three digits) and classes (four) were not taken”, which was a choice and not a licence term). `Industry.code` is `max_length=4` (`jobs/models.py:61`): it holds `85` and would hold `8569`, not `85.69`. - **What the classification says about who certifies people** (Eurostat, *NACE Rev. 2.1, 2025 edition*, KS-GQ-24-007-EN-N, explanatory notes; every code and title below was read there): - **85.59 Other education n.e.c.** includes “professional examination review courses” and “computer training (e.g. general computer use, programming courses, use of computer applications)”. - **85.69 Educational support activities n.e.c.** includes “educational testing evaluation activities” and “examination and testing of regulated professions (e.g. pilots)”. - **85.32 Vocational secondary education** covers “education activities of driving schools for occupational drivers” and schools for professional pilots; **85.53 Driving school activities** covers those “not issuing commercial certificates and permits”. - **94.12 Activities of professional membership organisations** includes “the establishment and supervision of standards of practice”. - **71.20 Technical testing and analysis** includes “certification of products”: products, not people, so it is not an awarding body in this sense. - A **vendor** that certifies its own users is classified by what it sells (hosting is 63.10 and programming is 62.10, both read), and a foundation or a company that trains is classified by that business too. Which class Eurostat gives a cloud vendor or a foundation such as the CNCF is **not verified**. - Education, 85, is section Q in Rev. 2.1 (section P in Rev. 2). 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.** `Certification.company`, a nullable foreign key to `jobs.Company`, `SET_NULL`, with `issuer` kept as the optional text every theme and file reads. The issuer box offers the person's companies as #683's does, in the order e1's registry gives for the role “awarding body”: the companies whose industries include division 85 or 94 first, then every other company below them. 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, **with no industry assigned**: a school can be classified from the form it was typed into, a vendor or a professional body cannot. 2. **The notice.** Where the chosen company has no industry with a NACE code at all, the entry says so in one line (“This company has none of its industries from the NACE list yet”) with a link to the company, which is the reading of “valid NACE code” recommended below. A company with a code outside 85 and 94 gets no notice. It never blocks saving. 3. **Files and history**, as #685: `company` (name or blank) in the archive and the candidate file, linked by name after the companies exist, offered as a link on the review page only on an exact match, never created from a file; the Europass import adds none; a migration links exact name matches only; the company page lists the certifications that name it; the merge moves them and the delete dialog counts them. ## Decisions - **What “valid NACE code” means.** (a) The company carries some valid Rev. 2.1 code, and the form says when it carries none; (b) its code is in a defined set of awarding bodies, and nothing else is accepted; (c) a set per kind of certification. **Recommended: (a), with the set {85, 94} used only to put likely issuers first.** The notes above show that people's certificates come from education and testing bodies (85), professional bodies (94), vendors in any division and public authorities (84, whose notes were not read for this), and a strict set would turn away the issuers most IT and trade certificates have. If the maintainer wants (b), the set is data in e1's registry and an issuer outside it has to be classified by hand first. - **Division or class.** Class-level precision (85.69 or 94.12) would need the 651 classes of Rev. 2.1 harvested from the Publications Office copy the vendored file came from (same licence, CC BY 4.0), a spelling for a class in `Industry.code`, and a larger file: the English-only titles of the 1,025 divisions, groups and classes come to 65 KB, against 177 KB for 109 entries in 24 languages today, so the full set in 24 languages would be well over a megabyte (an estimate, not measured). **Recommended: division level only**, as #671 does; nothing here needs a class. - **What a CV prints.** The issuer's own text, unchanged. A certification is the awarding body's wording and #131 kept it out of every translation; #673's per-CV switch for companies' names in the CV's language (see #683) should leave certifications out. **Recommended:** both. - **Optional stays optional.** A certification with no issuer is still valid, and one whose issuer is a person (a trainer) is text. Nothing forces a company. - **Where a credential's issuer is a body Postulo already knows.** #670 reads an awarding body from a European Digital Credential; once this exists it offers the exact-name link, and never adds a company from a file. ## Prior art No new dependency. The classification is Eurostat's NACE Rev. 2.1, 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. The classes quoted are from Eurostat, *NACE Rev. 2.1 — Statistical classification of economic activities in the European Union*, 2025 edition (doi:10.2785/155339). Not verified: how Eurostat's notes treat public authorities that issue licences (division 84), and which class a cloud vendor or a foundation falls in. ## Builds on Issues #131 (a certification translates nothing), #140 and #266, #683, #671. ## Needed by #670 (the issuer of a credential), #693 (awards and memberships name an organisation as a company too). ## Done when - `tests/test_career_companies.py` (new in #683) gains the certification cases: saving links or adds a company marked as from the career, never assigns an industry, never uses another owner's company; the migration links exact matches only; an entry with no issuer saves as before. - #671's registry test (a new file beside `tests/test_nace.py`): the “awarding body” role is divisions 85 and 94, every code in the registry exists in the vendored file; the order puts a company with “Education” or a membership-organisation industry first and loses none; the notice shows for a company with no coded industry, not for one with an industry outside 85 and 94, and never blocks. - `tests/test_career_languages.py` still passes unchanged (a certification translates nothing, `:83-85`, `:342-351`); `tests/test_help_text.py` follows the field. - `tests/test_merging.py` moves certifications; `tests/test_jobs_isolation.py` and `tests/security/test_isolation_sweep.py` show another account's company is neither offered nor linked. - `tests/test_export.py` and the archive round trip carry `company` on a certification at the new format; `tests/test_candidate_file.py` holds the new `SHAPES` line and `tests/security/test_candidate_file.py` a hostile `company`; `tests/test_importers.py` for the importer's pass. - 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 (#686).

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