Your career: courses, with a provider, a length and the certificate that proves them #695

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

The maintainer's list, under Your career, names “Courses”. A short course, a bootcamp, an evening class or an online programme is neither a degree nor a credential somebody can verify, so today it goes into Education with a made-up qualification or is left off. This issue adds a section for it, with a provider, a length and a way to point at the certificate a person holds, following the shape and the checklist settled in #692.

What exists today

  • No such record. The career's kinds are in src/postulo/resume/registry.py:28-80. The two that sit either side of a course are:
    • Education: a qualification, an institution, a field of study, a place, dates, a grade and highlights (src/postulo/resume/models.py:101-121), shown as a degree-shaped entry on a CV (src/postulo/templates/documents/themes/base_cv.html:135-137). #684 gives it an EQF level.
    • Certification: a name, an issuer, an issue date, an optional expiry and a credential link (:215-228), printed as “name, issuer” and the year (base_cv.html:117-125). #686 reopens its issuer.
  • A certificate can be stored, not attached to an entry. DocumentKind.CERTIFICATE is an upload-only kind (src/postulo/documents/kinds.py:61), and an uploaded document is tied to nothing in the career (src/postulo/documents/models.py:564-640). #669 proposes UploadedDocument.proves, a generic link to one of the owner's entries, limited to the models that can be proven (“Education and Certification now, a section later by adding one line”).
  • A provider is free text wherever the career names a body (Education.institution, Certification.issuer; #131, src/postulo/resume/translatable.py:16-26), and #685 and #686 are reopening that for the two of them.
  • No length anywhere. Nothing in the career stores hours, credits or a duration.
  • What Europass says. The Candidate format has no element for courses (read for #692). Its EducationHistory/EducationOrganizationAttendance is what the old template called Education and training, “add separate entries for each course” (Cedefop, Instructions for filling in the Europass CV, © European Union 2002-2017), and the plugin already reads each attendance as an Education entry (src/postulo/plugins/europass/candidate.py:473-497). The format before 2020 had an Achievement code courses holding one prose paragraph; the older instructions say to attach “any certificates issued at the end of training courses which did not lead to a formal qualification” (Cedefop, Instructions for using the Europass curriculum vitae, © European Union 2002-2010 as printed). That paragraph arrives today as a Project named after its heading (plugins/europass/reader.py:471-483).
  • A date's year is what a certification prints (documents/rendering.py:363-371); an education or experience entry prints the month and the year.

Checked against main at f09b6010b.

Proposal

  1. One model, Course (“Course”; heading “Courses”). A ResumeItem with: title (required, 200); provider (optional text, 200: a school, a company, a platform, an employer's own academy); start_date and end_date (optional); hours (optional whole number from 1 to 10,000: the hours of teaching or study as the provider states them); summary (optional, a line or two: what it covered); url (optional: a public page for the course or its certificate). Slug course, placed in OVERVIEW_ORDER where #692 puts it. A new entry is placed by end_date, as an education entry is (ordering.DATE_FIELDS).
  2. Where a course ends and the other sections begin, said in the form's help and in the wiki, once:
    • Education is a programme that leads to a qualification in a national or European framework: a degree, a diploma, a vocational qualification.
    • Certification is a credential somebody can check and that may lapse: an exam passed, a licence, a vendor's certificate.
    • A course is learning that was taken, with or without a certificate of attendance or completion. If it ended in a checkable credential, the credential is a certification and the course may be mentioned in its summary; if it ended in a degree it is education.
    • An event one took part in, spoke at or helped run is #694.
  3. The certificate, when #669 has landed: Course joins the models a document may prove (one line there), the form on the document side offers the owner's courses, and a course's row on the career page says how many documents prove it and links to them. A diploma-like file is not what is meant here: an attendance or completion certificate is an upload of kind Certificate. Until d4 lands, url is the only pointer.
  4. The CV prints “title, provider” then the length and the year: “40 hours” (a plural-aware message in the document's language) and the year or the two years. Both base themes, the plain text and the Word file print the same line. The summary prints under the line, which a certification's does not (a decision below).
  5. Translations: summary only. A course's title and its provider are the provider's wording, by the rule that keeps a credential's name as it is.
  6. Archive and candidate file: block courses, in RESUME_FIELDS, RESUME_MODELS and TRANSLATION_SECTIONS; the importer's maps; a Kind whose names are the title and the provider and whose dates are the two dates. Both formats go up by one from whatever they are when the commit lands. hours is read through the form's own bounds, so a file cannot store a value the page would refuse.
  7. Europass. No reading is added. The Candidate has no element for a course and the plugin already files each EducationOrganizationAttendance as education; deciding which of those are really courses would be a guess, and an import saves nothing on one. A course the person typed into a Europass Others section stays a Project, as today. The record is shaped so an exporter could write one as an EducationOrganizationAttendance with no degree.
  8. Search by title and provider, linking to #section-course. Demo data: a course with hours and one without. Wiki: Your career record gains the section and the rule in step 2.
  9. After #683, the company link: Course.company, nullable, SET_NULL, beside provider, 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, and a software company's academy is not in education), a file never creates a company, a company merge moves the links to the kept company and the company's delete dialog counts them (jobs/merging.py, jobs/views.py:574-610); both formats go up by one again.

Decisions

These are recommendations the maintainer has not confirmed.

  • A provider: text or a company. The note asks for companies as the institution of an education entry and the issuer of a certification and says nothing of courses; #683 expects courses to follow. Providers vary more than either: a university, a training firm, a platform, a manufacturer, an employer. Recommended: the rule in #692. Text first, with nothing here waiting on g1 or #685; 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 and a company the form creates carrying “from the career”, so a provider never becomes somewhere to apply. #685's “place of learning” role (division 85, Education, section Q in src/postulo/jobs/data/nace-2.1.json) orders the list and restricts nothing: a software company's academy sits in no education division.
  • Hours or a duration or credits. Options: whole hours; a free text such as “6 weeks”; ECTS credits. Recommended: hours, optional. It is the one figure the old Europass instructions and most certificates give, it prints in a plural-aware sentence, and it needs no unit table. Credits belong to education, and a free text cannot be bounded, translated or sorted. The cost is that a course given in weeks needs the person to say the hours, or leave them out.
  • A certificate beyond a link. Recommended: no column on the course. The file is a document (#669's link points at the course, from the document), and the course's own url is only an address, not a promise the address stays up; the career page never fetches it, as it never fetches a link without being asked (the Check button, resume/links.py).
  • Does a summary print. A certification line is short; a course is often listed for what it covered. Recommended: print it, under the line, in both base themes; a person who wants a short CV leaves it empty, or leaves the entry off that CV, as with every entry.
  • Courses in Education. People list courses there today. Recommended: no automatic move. An entry stays where it was put; the wiki's rule is the guidance, and a person can retype one.
  • Is a MOOC with a certificate a course or a certification. Recommended rule: a course, with the certificate as a document; a certification only where the body offers a way to check the credential and the person means it as one. The rule is in the form's help rather than enforced.
  • EDC. A European Digital Credential for a short course (#670) would describe a learning achievement. Recommended: nothing here depends on it; if it is read later, a course is where a credential for a non-degree learning outcome lands.

Prior art

No dependency. The standard is Europass: no element for courses in the 2020 Candidate format, the courses Achievement code and the template headings before it, as read for #692. Whether the current portal offers a Courses section by that name, and what it writes into the file for one, is not verified (its FAQ says only that sections can be added with a title the person chooses).

Builds on

Issues #131 (entries in other languages; free-text bodies), #181 (the candidate file), #203 (the order of entries), #244 and #318 (the Europass reader and its real-download check). New issues of this set: #692 (shape and checklist), #694 (events), #669 (a document that proves an entry), #685 and #686 (institution and issuer as companies), #683, #671, #684 (EQF on education).

Needed by

Nothing waits on it. #669 gains Course in its list of provable models when both have landed.

Done when

  • A new tests/test_courses.py: create, edit, delete and move through the registry; hours accepts 1 to 10,000 and refuses 0, a negative, text and a number beyond the bound; the CV prints “title, provider”, the hours in a plural-aware message and the year in the document's language in the HTML themes, the plain text and the Word file, a portfolio prints it without an empty heading, and an entry with only a title prints just that; a new entry lands by its end date; the form's help carries the rule against education and certification.
  • tests/test_career_sections.py (the #692 guard) passes with the section; tests/test_career_languages.py gains “a course translates its summary, not its title or provider”.
  • After #683: tests/test_merging.py moves the links of courses, tests/test_career_companies.py gains the course cases, and a file never creates a company.
  • If #669 has landed: a certificate uploaded and pointed at a course shows on its row, and another account's course is refused by the form and by the save (tests/test_documents.py).
  • 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 the commit lands) with a round trip in tests/test_export.py, a CV holding one and an older archive that still loads; the candidate format (4) goes up by one with its line in SHAPES (tests/test_candidate_file.py:372-387), a review row for a good entry and a refused one (an hours value out of bounds), and tests/security/test_candidate_file.py for hostile values; tests/test_europass_candidate.py asserts a course is not invented from an education entry; the API is unchanged (there is none for the career); search finds one (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; 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 them (tests/test_account_deletion.py); strings, including the plural message, are in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349).
  • The wiki and the CHANGELOG line (ends (#N)) are in the same commit.

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

The maintainer's list, under Your career, names “Courses”. A short course, a bootcamp, an evening class or an online programme is neither a degree nor a credential somebody can verify, so today it goes into Education with a made-up qualification or is left off. This issue adds a section for it, with a provider, a length and a way to point at the certificate a person holds, following the shape and the checklist settled in #692. ## What exists today - **No such record.** The career's kinds are in `src/postulo/resume/registry.py:28-80`. The two that sit either side of a course are: - `Education`: a qualification, an institution, a field of study, a place, dates, a grade and highlights (`src/postulo/resume/models.py:101-121`), shown as a degree-shaped entry on a CV (`src/postulo/templates/documents/themes/base_cv.html:135-137`). #684 gives it an EQF level. - `Certification`: a name, an issuer, an issue date, an optional expiry and a credential link (`:215-228`), printed as “name, issuer” and the year (`base_cv.html:117-125`). #686 reopens its issuer. - **A certificate can be stored, not attached to an entry.** `DocumentKind.CERTIFICATE` is an upload-only kind (`src/postulo/documents/kinds.py:61`), and an uploaded document is tied to nothing in the career (`src/postulo/documents/models.py:564-640`). #669 proposes `UploadedDocument.proves`, a generic link to one of the owner's entries, limited to the models that can be proven (“`Education` and `Certification` now, a section later by adding one line”). - **A provider is free text** wherever the career names a body (`Education.institution`, `Certification.issuer`; #131, `src/postulo/resume/translatable.py:16-26`), and #685 and #686 are reopening that for the two of them. - **No length anywhere.** Nothing in the career stores hours, credits or a duration. - **What Europass says.** The Candidate format has no element for courses (read for #692). Its `EducationHistory/EducationOrganizationAttendance` is what the old template called *Education and training*, “add separate entries for each course” (Cedefop, *Instructions for filling in the Europass CV*, © European Union 2002-2017), and the plugin already reads each attendance as an Education entry (`src/postulo/plugins/europass/candidate.py:473-497`). The format before 2020 had an `Achievement` code `courses` holding one prose paragraph; the older instructions say to attach “any certificates issued at the end of training courses which did not lead to a formal qualification” (Cedefop, *Instructions for using the Europass curriculum vitae*, © European Union 2002-2010 as printed). That paragraph arrives today as a Project named after its heading (`plugins/europass/reader.py:471-483`). - **A date's year is what a certification prints** (`documents/rendering.py:363-371`); an education or experience entry prints the month and the year. Checked against `main` at `f09b6010b`. ## Proposal 1. **One model, `Course`** (“Course”; heading “Courses”). A `ResumeItem` with: `title` (required, 200); `provider` (optional text, 200: a school, a company, a platform, an employer's own academy); `start_date` and `end_date` (optional); `hours` (optional whole number from 1 to 10,000: the hours of teaching or study as the provider states them); `summary` (optional, a line or two: what it covered); `url` (optional: a public page for the course or its certificate). Slug `course`, placed in `OVERVIEW_ORDER` where #692 puts it. A new entry is placed by `end_date`, as an education entry is (`ordering.DATE_FIELDS`). 2. **Where a course ends and the other sections begin**, said in the form's help and in the wiki, once: - *Education* is a programme that leads to a qualification in a national or European framework: a degree, a diploma, a vocational qualification. - *Certification* is a credential somebody can check and that may lapse: an exam passed, a licence, a vendor's certificate. - *A course* is learning that was taken, with or without a certificate of attendance or completion. If it ended in a checkable credential, the credential is a certification and the course may be mentioned in its summary; if it ended in a degree it is education. - An *event* one took part in, spoke at or helped run is #694. 3. **The certificate**, when #669 has landed: `Course` joins the models a document may prove (one line there), the form on the document side offers the owner's courses, and a course's row on the career page says how many documents prove it and links to them. A diploma-like file is not what is meant here: an attendance or completion certificate is an upload of kind *Certificate*. Until d4 lands, `url` is the only pointer. 4. **The CV** prints “title, provider” then the length and the year: “40 hours” (a plural-aware message in the document's language) and the year or the two years. Both base themes, the plain text and the Word file print the same line. The summary prints under the line, which a certification's does not (a decision below). 5. **Translations**: `summary` only. A course's title and its provider are the provider's wording, by the rule that keeps a credential's name as it is. 6. **Archive and candidate file**: block `courses`, in `RESUME_FIELDS`, `RESUME_MODELS` and `TRANSLATION_SECTIONS`; the importer's maps; a `Kind` whose `names` are the title and the provider and whose `dates` are the two dates. Both formats go up by one from whatever they are when the commit lands. `hours` is read through the form's own bounds, so a file cannot store a value the page would refuse. 7. **Europass.** No reading is added. The Candidate has no element for a course and the plugin already files each `EducationOrganizationAttendance` as education; deciding which of those are really courses would be a guess, and an import saves nothing on one. A course the person typed into a Europass `Others` section stays a Project, as today. The record is shaped so an exporter could write one as an `EducationOrganizationAttendance` with no degree. 8. **Search** by title and provider, linking to `#section-course`. **Demo data**: a course with hours and one without. **Wiki**: *Your career record* gains the section and the rule in step 2. 9. **After #683, the company link**: `Course.company`, nullable, `SET_NULL`, beside `provider`, 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, and a software company's academy is not in education), a file never creates a company, a company merge moves the links to the kept company and the company's delete dialog counts them (`jobs/merging.py`, `jobs/views.py:574-610`); both formats go up by one again. ## Decisions These are recommendations the maintainer has not confirmed. - **A provider: text or a company.** The note asks for companies as the institution of an education entry and the issuer of a certification and says nothing of courses; #683 expects courses to follow. Providers vary more than either: a university, a training firm, a platform, a manufacturer, an employer. **Recommended: the rule in #692.** Text first, with nothing here waiting on g1 or #685; 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 and a company the form creates carrying “from the career”, so a provider never becomes somewhere to apply. #685's “place of learning” role (division 85, Education, section Q in `src/postulo/jobs/data/nace-2.1.json`) orders the list and restricts nothing: a software company's academy sits in no education division. - **Hours or a duration or credits.** Options: whole hours; a free text such as “6 weeks”; ECTS credits. **Recommended: hours, optional.** It is the one figure the old Europass instructions and most certificates give, it prints in a plural-aware sentence, and it needs no unit table. Credits belong to education, and a free text cannot be bounded, translated or sorted. The cost is that a course given in weeks needs the person to say the hours, or leave them out. - **A certificate beyond a link.** **Recommended: no column on the course.** The file is a document (#669's link points at the course, from the document), and the course's own `url` is only an address, not a promise the address stays up; the career page never fetches it, as it never fetches a link without being asked (the *Check* button, `resume/links.py`). - **Does a summary print.** A certification line is short; a course is often listed for what it covered. **Recommended: print it, under the line, in both base themes**; a person who wants a short CV leaves it empty, or leaves the entry off that CV, as with every entry. - **Courses in Education.** People list courses there today. **Recommended:** no automatic move. An entry stays where it was put; the wiki's rule is the guidance, and a person can retype one. - **Is a MOOC with a certificate a course or a certification.** **Recommended rule:** a course, with the certificate as a document; a certification only where the body offers a way to check the credential and the person means it as one. The rule is in the form's help rather than enforced. - **EDC.** A European Digital Credential for a short course (#670) would describe a learning achievement. **Recommended:** nothing here depends on it; if it is read later, a course is where a credential for a non-degree learning outcome lands. ## Prior art No dependency. The standard is Europass: no element for courses in the 2020 Candidate format, the `courses` `Achievement` code and the template headings before it, as read for #692. Whether the current portal offers a *Courses* section by that name, and what it writes into the file for one, is not verified (its FAQ says only that sections can be added with a title the person chooses). ## Builds on Issues #131 (entries in other languages; free-text bodies), #181 (the candidate file), #203 (the order of entries), #244 and #318 (the Europass reader and its real-download check). New issues of this set: #692 (shape and checklist), #694 (events), #669 (a document that proves an entry), #685 and #686 (institution and issuer as companies), #683, #671, #684 (EQF on education). ## Needed by Nothing waits on it. #669 gains `Course` in its list of provable models when both have landed. ## Done when - A new `tests/test_courses.py`: create, edit, delete and move through the registry; `hours` accepts 1 to 10,000 and refuses 0, a negative, text and a number beyond the bound; the CV prints “title, provider”, the hours in a plural-aware message and the year in the document's language in the HTML themes, the plain text and the Word file, a portfolio prints it without an empty heading, and an entry with only a title prints just that; a new entry lands by its end date; the form's help carries the rule against education and certification. - `tests/test_career_sections.py` (the #692 guard) passes with the section; `tests/test_career_languages.py` gains “a course translates its summary, not its title or provider”. - After #683: `tests/test_merging.py` moves the links of courses, `tests/test_career_companies.py` gains the course cases, and a file never creates a company. - If #669 has landed: a certificate uploaded and pointed at a course shows on its row, and another account's course is refused by the form and by the save (`tests/test_documents.py`). - 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 the commit lands) with a round trip in `tests/test_export.py`, a CV holding one and an older archive that still loads; the candidate format (4) goes up by one with its line in `SHAPES` (`tests/test_candidate_file.py:372-387`), a review row for a good entry and a refused one (an hours value out of bounds), and `tests/security/test_candidate_file.py` for hostile values; `tests/test_europass_candidate.py` asserts a course is not invented from an education entry; the API is unchanged (there is none for the career); search finds one (`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; `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 them (`tests/test_account_deletion.py`); strings, including the plural message, are in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349). - The wiki and the CHANGELOG line (ends `(#N)`) are in the same commit. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.6.0 milestone 2026-10-03 01:59:07 +00:00
Author
Owner

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

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