An education entry states its EQF level, as a number from 1 to 8 #684

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

The maintainer's list says, under Your career: “Learning can have EQF level or equivalent.” Postulo has nowhere to put a European Qualifications Framework level: the one place it survives is the text “EQF 6”, written into the grade by the Europass import, and only when the file gives no grade.

What exists today

  • No field. Education holds the institution, the qualification, the field of study, a place, two dates, a free-text grade (100 characters) and highlights (src/postulo/resume/models.py:101-121). grep finds EQF only in the Europass reader.
  • The Europass import writes the level into the grade, and loses it when there is a grade. The Candidate reader takes EducationLevelCode where the list is the EQF one, with ^(?:EQF[\s_-]*)?([1-8])$, and returns "EQF 6", but only when the file gives no final grade (the docstring: the grade “where the file gives one, and otherwise the EQF level”): a final grade wins and the level is dropped (src/postulo/plugins/europass/candidate.py:154, :500-521; tests/test_europass_candidate.py:339-347, :349-365). A bare number on the ISCED list is not taken as EQF (:367-377). The older XML and JSON readers copy the format's Level label into the grade as it is, whatever it says (plugins/europass/reader.py:429, :704; tests/data/europass.xml:48 has EQF 7). The Candidate schema offers two scales for this code, ISCED-2011 or EQF (tests/data/europass-candidate-sample.xml:115-119).
  • No theme prints a grade, and the form says one does. base_cv.html:136-138 prints the institution and the place and nothing else of an education entry; base_portfolio.html:139-143 the same; the text and the Word file are held to what the page prints (documents/rendering.py:303-319, tests/test_document_formats.py:195-205, wiki CVs: “Something the themes do not print — a grade, a field of study — is not in the text either”). Yet the grade's help says “As the institution words it. It prints only if you fill it in” (resume/forms.py:122). A level that “shows on the CV” therefore needs the themes to learn a new line, not only a new column.
  • The career block is shared. RESUME_FIELDS["education"] (core/export.py:355-366) is written into the archive and into the candidate file by the same code, and tests/test_candidate_file.py:354-387 fingerprints it, so a new field bumps both formats (FORMAT_VERSION 29 and CANDIDATE_FORMAT 4 at f09b6010b). A candidate file is read through EducationForm (resume/candidate.py:198-217); the archive importer is not, and hands every key to model.objects.create (core/importer.py:556), so a value outside 1 to 8 would be stored as written.
  • grade is translatable per CV language (resume/translatable.py:29); a level is a number and is not.
  • Where a CV chooses what it prints is documents/printing.py (#308): a yes-or-no per detail in SWITCHES (:156), each a column on CV (documents/models.py:316-326), looped over by the CV form (documents/forms.py:294), the API (api/routers/documents.py:78, :191) and the archive importer (core/importer.py:955). The themes are handed the cv itself (documents/rendering.py:259-271).
  • The API has no career endpoints (src/postulo/api/routers/).

Checked against main at f09b6010b.

Proposal

Two commits, in this order.

  1. The field. Education.eqf_level, a nullable small integer with the choices 1 to 8 and “Not stated” first, in the form beside the qualification (“EQF level”, with a sentence: the level of the qualification in the European Qualifications Framework, which a diploma or its supplement may state; Postulo never works it out from the qualification's name, as it does not for a language level, #235). Never translated. Shown on the career preview (templates/resume/preview.html:51). The archive and the candidate file carry it in RESUME_FIELDS["education"], both formats up by one; the candidate reader takes it through the form, so a nine or a word is a refused row on the review page; the archive importer keeps an integer from 1 to 8 and drops anything else with a line in its report.
  2. The Europass import and what a CV prints.
    • The Candidate reader puts an EQF code in eqf_level and leaves the grade alone, so a file with a grade and a level keeps both; the older readers do the same for a Level that is exactly “EQF n”, and keep any other label in the grade as before. Nothing is read from an ISCED code.
    • A switch on the CV, “print the EQF level of my qualifications”, off, joined to printing.SWITCHES with its column, form, API and importer entries (the API's CVPrintsOut and CVPrintsIn name each switch, api/schemas.py:739-753 and :784-795, so both gain a line), and written in the archive's prints block of the CV (printing.as_archived), which is a second archive bump beside the one for the field. The themes (base_cv.html, base_portfolio.html), the preview, the plain text and the Word file print one extra element on an education entry, “EQF level 7”, beside the institution and the place, in the document's language, only where the switch is on and the level is set. Plugin themes read entry.item.eqf_level and cv.<switch> on their own and need no new name on postulo.plugins.api.
    • A data migration that moves an exact “EQF 1” to “EQF 8” from the grade into the level and clears the grade, which is the one text the importer ever wrote; any other grade is left alone, and so are the translations of a grade that was not moved. Nothing prints newly, since the switch is off on every CV.
    • The wiki's Your career record and CVs say what the field is and that it prints under a switch. The grade's help is corrected to what is true (decision below).

Decisions

  • EQF only, or a national level as well. The list says “EQF level or equivalent”. What was verified: the EQF is an eight-level, learning-outcomes framework that serves as “a translation tool between different national qualifications frameworks”, and states that national levels are referenced to it, after which newly issued qualifications “should in principle contain a clear reference to the appropriate EQF and NQF level” (Europass, The European Qualifications Framework). Portugal's QNQ has eight levels and is referenced to the EQF (2011); France's Cadre national des certifications professionnelles has eight levels since 2019 and was referenced in 2021 (both from Cedefop's NQF online tool as summarised by a search; the pages refused an automated fetch, so confirm before building on them). The UK is not on Europass's comparison page, and GOV.UK's list of levels for England, Wales and Northern Ireland (entry level and 1 to 8, FHEQ and RQF) says nothing about the EQF; Scotland's SCQF and the status of UK referencing after 2020 were not verified. Germany, Ireland, Spain and the rest were not checked. Recommended: the EQF level only in this issue. A national field means a registry of frameworks with a source and an owner to keep it current, and equivalences stated by Postulo and not by the framework's own referencing report would be a guess. Someone who wants “QNQ 6” or “FHEQ 6” on a CV writes it in the qualification, as they do today. Revisit with the maintainer once a country is chosen.
  • ISCED. The Candidate schema allows ISCED-2011 in the same place, and a bare number there is a different scale. Recommended: not stored; the importer keeps ignoring it.
  • A switch, or print wherever it is filled in. Recommended: a switch, off, as #308 did for the details of a profile: a CV that nobody has opened stays what it was, and an EQF level means little on a CV for a country outside the framework's reach, so one CV may want it and another not. Printing wherever filled in is simpler and is the convention of the dates; the cost is that an imported level would start appearing on a CV already sent.
  • The grade's help. It promises a print no theme makes. Recommended: correct the sentence now (“Kept in your record; a CV does not print it yet”), and leave printing the grade, and the field of study, for the maintainer to ask for. Found on the way; not part of the list.
  • Which entries. Education only. A certification or a course may state a level too, and the European Digital Credential does (#670 reads one); neither is asked for here.

Prior art

No library: eight integers. Standards: the EQF, Council Recommendation of 22 May 2017 on the European Qualifications Framework for lifelong learning (2017/C 189/03, OJ C 189, 15.6.2017; repeals the 2008 Recommendation, which is the listVersionID="2008/C11/01" the Candidate sample still names); the Europass Candidate code lists (ISCED-2011 and EQF). The text of the 2017 Recommendation could not be fetched for this draft (EUR-Lex refused the automated request); its title and reference come from its EUR-Lex and Publications Office listings.

Builds on

Issues #235 (a level nobody stated prints nothing), #181 (the candidate file), #308 (what a CV prints), #244 and #318 (the Europass reader and its check against real files).

Needed by

#669 (a diploma kept as a document may show the level of the entry it proves) and #670 (a credential's EQF level lands in this field).

Done when

  • A new test in tests/test_career_sections.py or a tests/test_eqf_level.py: the form saves and clears a level, refuses 0 and 9, never translates it (tests/test_career_languages.py fields list), the preview shows it.
  • tests/test_europass_candidate.py: a file with a final grade and an EQF code keeps both; an ISCED code sets nothing; the older readers move exactly “EQF n” and leave other labels in the grade (tests/test_europass.py).
  • tests/test_cv_prints.py and tests/security/test_cv_prints.py for the switch (default off, chosen, in the archive and in the API's CV answer and patch, tests/test_api.py); tests/test_documents.py and tests/test_document_formats.py: with the switch off the page, the text and the Word file are what they were, with it on all three print the same line, and test_every_line_of_the_text_is_on_the_page still holds.
  • tests/test_export.py and the archive round trip carry the field at the new format; tests/test_candidate_file.py holds the new SHAPES line and a refused row for a level of 9 (tests/security/test_candidate_file.py with a hostile value); tests/test_importers.py drops an out-of-range value with a report line. A migration test for “EQF 6” moved and “6.0 GPA” left.
  • No career API (none exists) beyond the CV switch above, no search, no new page, no line in the processing record (core/gdpr.py). The standing checklist otherwise applies: archive format one more than whatever it is when each commit lands (29 at f09b6010b; the field and the switch are two bumps), candidate format likewise for the field (4; the switch is not in the candidate file), strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349). The abbreviation of the framework's name in each language is for the translators to choose and was not verified here.

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

The maintainer's list says, under Your career: “Learning can have EQF level or equivalent.” Postulo has nowhere to put a European Qualifications Framework level: the one place it survives is the text “EQF 6”, written into the grade by the Europass import, and only when the file gives no grade. ## What exists today - **No field.** `Education` holds the institution, the qualification, the field of study, a place, two dates, a free-text `grade` (100 characters) and highlights (`src/postulo/resume/models.py:101-121`). `grep` finds EQF only in the Europass reader. - **The Europass import writes the level into the grade, and loses it when there is a grade.** The Candidate reader takes `EducationLevelCode` where the list is the EQF one, with `^(?:EQF[\s_-]*)?([1-8])$`, and returns `"EQF 6"`, but only when the file gives no final grade (the docstring: the grade “where the file gives one, and otherwise the EQF level”): a final grade wins and the level is dropped (`src/postulo/plugins/europass/candidate.py:154`, `:500-521`; `tests/test_europass_candidate.py:339-347`, `:349-365`). A bare number on the ISCED list is not taken as EQF (`:367-377`). The older XML and JSON readers copy the format's `Level` label into the grade as it is, whatever it says (`plugins/europass/reader.py:429`, `:704`; `tests/data/europass.xml:48` has `EQF 7`). The Candidate schema offers two scales for this code, ISCED-2011 or EQF (`tests/data/europass-candidate-sample.xml:115-119`). - **No theme prints a grade, and the form says one does.** `base_cv.html:136-138` prints the institution and the place and nothing else of an education entry; `base_portfolio.html:139-143` the same; the text and the Word file are held to what the page prints (`documents/rendering.py:303-319`, `tests/test_document_formats.py:195-205`, wiki *CVs*: “Something the themes do not print — a grade, a field of study — is not in the text either”). Yet the grade's help says “As the institution words it. It prints only if you fill it in” (`resume/forms.py:122`). A level that “shows on the CV” therefore needs the themes to learn a new line, not only a new column. - **The career block is shared.** `RESUME_FIELDS["education"]` (`core/export.py:355-366`) is written into the archive and into the candidate file by the same code, and `tests/test_candidate_file.py:354-387` fingerprints it, so a new field bumps both formats (`FORMAT_VERSION` 29 and `CANDIDATE_FORMAT` 4 at `f09b6010b`). A candidate file is read through `EducationForm` (`resume/candidate.py:198-217`); the archive importer is not, and hands every key to `model.objects.create` (`core/importer.py:556`), so a value outside 1 to 8 would be stored as written. - **`grade` is translatable** per CV language (`resume/translatable.py:29`); a level is a number and is not. - **Where a CV chooses what it prints** is `documents/printing.py` (#308): a yes-or-no per detail in `SWITCHES` (`:156`), each a column on `CV` (`documents/models.py:316-326`), looped over by the CV form (`documents/forms.py:294`), the API (`api/routers/documents.py:78`, `:191`) and the archive importer (`core/importer.py:955`). The themes are handed the `cv` itself (`documents/rendering.py:259-271`). - **The API has no career endpoints** (`src/postulo/api/routers/`). Checked against `main` at `f09b6010b`. ## Proposal Two commits, in this order. 1. **The field.** `Education.eqf_level`, a nullable small integer with the choices 1 to 8 and “Not stated” first, in the form beside the qualification (“EQF level”, with a sentence: the level of the qualification in the European Qualifications Framework, which a diploma or its supplement may state; Postulo never works it out from the qualification's name, as it does not for a language level, #235). Never translated. Shown on the career preview (`templates/resume/preview.html:51`). The archive and the candidate file carry it in `RESUME_FIELDS["education"]`, both formats up by one; the candidate reader takes it through the form, so a nine or a word is a refused row on the review page; the archive importer keeps an integer from 1 to 8 and drops anything else with a line in its report. 2. **The Europass import and what a CV prints.** - The Candidate reader puts an EQF code in `eqf_level` and leaves the grade alone, so a file with a grade and a level keeps both; the older readers do the same for a `Level` that is exactly “EQF n”, and keep any other label in the grade as before. Nothing is read from an ISCED code. - A switch on the CV, “print the EQF level of my qualifications”, off, joined to `printing.SWITCHES` with its column, form, API and importer entries (the API's `CVPrintsOut` and `CVPrintsIn` name each switch, `api/schemas.py:739-753` and `:784-795`, so both gain a line), and written in the archive's `prints` block of the CV (`printing.as_archived`), which is a second archive bump beside the one for the field. The themes (`base_cv.html`, `base_portfolio.html`), the preview, the plain text and the Word file print one extra element on an education entry, “EQF level 7”, beside the institution and the place, in the document's language, only where the switch is on and the level is set. Plugin themes read `entry.item.eqf_level` and `cv.<switch>` on their own and need no new name on `postulo.plugins.api`. - A data migration that moves an exact “EQF 1” to “EQF 8” from the grade into the level and clears the grade, which is the one text the importer ever wrote; any other grade is left alone, and so are the translations of a grade that was not moved. Nothing prints newly, since the switch is off on every CV. - The wiki's *Your career record* and *CVs* say what the field is and that it prints under a switch. The grade's help is corrected to what is true (decision below). ## Decisions - **EQF only, or a national level as well.** The list says “EQF level or equivalent”. What was verified: the EQF is an eight-level, learning-outcomes framework that serves as “a translation tool between different national qualifications frameworks”, and states that national levels are referenced to it, after which newly issued qualifications “should in principle contain a clear reference to the appropriate EQF and NQF level” (Europass, *The European Qualifications Framework*). Portugal's QNQ has eight levels and is referenced to the EQF (2011); France's *Cadre national des certifications professionnelles* has eight levels since 2019 and was referenced in 2021 (both from Cedefop's NQF online tool as summarised by a search; the pages refused an automated fetch, so confirm before building on them). The UK is not on Europass's comparison page, and GOV.UK's list of levels for England, Wales and Northern Ireland (entry level and 1 to 8, FHEQ and RQF) says nothing about the EQF; Scotland's SCQF and the status of UK referencing after 2020 were not verified. Germany, Ireland, Spain and the rest were not checked. **Recommended: the EQF level only in this issue.** A national field means a registry of frameworks with a source and an owner to keep it current, and equivalences stated by Postulo and not by the framework's own referencing report would be a guess. Someone who wants “QNQ 6” or “FHEQ 6” on a CV writes it in the qualification, as they do today. Revisit with the maintainer once a country is chosen. - **ISCED.** The Candidate schema allows ISCED-2011 in the same place, and a bare number there is a different scale. **Recommended: not stored**; the importer keeps ignoring it. - **A switch, or print wherever it is filled in.** **Recommended: a switch, off,** as #308 did for the details of a profile: a CV that nobody has opened stays what it was, and an EQF level means little on a CV for a country outside the framework's reach, so one CV may want it and another not. Printing wherever filled in is simpler and is the convention of the dates; the cost is that an imported level would start appearing on a CV already sent. - **The grade's help.** It promises a print no theme makes. **Recommended:** correct the sentence now (“Kept in your record; a CV does not print it yet”), and leave printing the grade, and the field of study, for the maintainer to ask for. Found on the way; not part of the list. - **Which entries.** Education only. A certification or a course may state a level too, and the European Digital Credential does (#670 reads one); neither is asked for here. ## Prior art No library: eight integers. Standards: the EQF, Council Recommendation of 22 May 2017 on the European Qualifications Framework for lifelong learning (2017/C 189/03, OJ C 189, 15.6.2017; repeals the 2008 Recommendation, which is the `listVersionID="2008/C11/01"` the Candidate sample still names); the Europass Candidate code lists (ISCED-2011 and EQF). The text of the 2017 Recommendation could not be fetched for this draft (EUR-Lex refused the automated request); its title and reference come from its EUR-Lex and Publications Office listings. ## Builds on Issues #235 (a level nobody stated prints nothing), #181 (the candidate file), #308 (what a CV prints), #244 and #318 (the Europass reader and its check against real files). ## Needed by #669 (a diploma kept as a document may show the level of the entry it proves) and #670 (a credential's EQF level lands in this field). ## Done when - A new test in `tests/test_career_sections.py` or a `tests/test_eqf_level.py`: the form saves and clears a level, refuses 0 and 9, never translates it (`tests/test_career_languages.py` fields list), the preview shows it. - `tests/test_europass_candidate.py`: a file with a final grade and an EQF code keeps both; an ISCED code sets nothing; the older readers move exactly “EQF n” and leave other labels in the grade (`tests/test_europass.py`). - `tests/test_cv_prints.py` and `tests/security/test_cv_prints.py` for the switch (default off, chosen, in the archive and in the API's CV answer and patch, `tests/test_api.py`); `tests/test_documents.py` and `tests/test_document_formats.py`: with the switch off the page, the text and the Word file are what they were, with it on all three print the same line, and `test_every_line_of_the_text_is_on_the_page` still holds. - `tests/test_export.py` and the archive round trip carry the field at the new format; `tests/test_candidate_file.py` holds the new `SHAPES` line and a refused row for a level of 9 (`tests/security/test_candidate_file.py` with a hostile value); `tests/test_importers.py` drops an out-of-range value with a report line. A migration test for “EQF 6” moved and “6.0 GPA” left. - No career API (none exists) beyond the CV switch above, no search, no new page, no line in the processing record (`core/gdpr.py`). The standing checklist otherwise applies: archive format one more than whatever it is when each commit lands (29 at `f09b6010b`; the field and the switch are two bumps), candidate format likewise for the field (4; the switch is not in the candidate file), strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349). The abbreviation of the framework's name in each language is for the translators to choose and was not verified here. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.6.0 milestone 2026-10-03 01:58:59 +00:00
Author
Owner

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

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