An experience names its organisation as a company #683
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#683
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The maintainer's list says, under Your career: “Experience organization is a Company.” Today the organisation of an experience is a line of free text, and that was chosen on purpose (#131, and the model's own comment); this issue changes the decision, keeps what it was protecting, and makes the entry point at a company of the person's own.
What exists today
Experience.organisationis a requiredCharField(200); the docstring gives the reason: “Where you have worked and where you are applying are unrelated lists, and joining them would put former employers into the company picker for new applications” (src/postulo/resume/models.py:65-74). The wiki tells the person the same (Your-career-record.md, “The organisations you have worked for are kept separately from the companies you are applying to”).28551c13) leftorganisationandinstitutionout of the career's translations: “Universidade de Lisboa stays that on an English CV; translating it invents an employer who never existed” (src/postulo/resume/translatable.py:16-27). Tests pin it:tests/test_career_languages.py:72-79(the field is not translatable),:305-312(the screen has no box for it) andtests/test_candidate_file.py:1302(a file that translates it is refused). They pin a free-text field, not a company, and stay true while the text stays.resume/forms.py:63-88); the CV's two themes (templates/documents/themes/base_cv.html:134,base_portfolio.html:139), which a plugin's theme replaces and reads asentry.item.organisation(documents/rendering.py:88-98says a plugin's template written againstentry.item.role“gets the French job title for free”, and the same holds here); the preview, the plain text and the Word file (documents/rendering.py:308,:379); search (core/search.py:468-481); the career preview (templates/resume/preview.html:25); the candidate file, where an experience is “its organisation, its title and its dates” (resume/candidate.py:178-199); the Europass import, which creates text only (resume/importing.py:224-235);seed_demo.RESUME_FIELDS["experience"](core/export.py:343-354) is written by_resume_blockinto both documents, andtests/test_candidate_file.py:354-387fingerprints it: a field added for the archive changes the candidate file too and fails untilCANDIDATE_FORMATis bumped (4 atf09b6010b;FORMAT_VERSIONis 29).core/importer.py:530-575, companies from:597), and hands every key of an entry straight to the constructor (model.objects.create(owner=user, **values),:556). A key the model does not take has to be popped first, asesco_uriis (:569). Where the archive already points at a company it does so by name, resolved in a pass after the companies exist (through_agency,:766-768,:888-899).Companyis owner-scoped, unique by name per owner, with notes, kind, industries, identifiers and a logo (jobs/models.py:238-313). One entry point already matches a typed name to a company case-insensitively and adds it if none exists:get_or_create_company(applications/services.py:305-335). The capture form's datalist of company names comes fromjobs/recall.py:44-65(applications/forms.py:182).jobs/forms.py:638,:731), the application's agency picker (applications/forms.py:423), the suggestions above, the companies table, the map (jobs/views.py:166-176) and search (core/search.py:247). The one precedent for narrowing is a kind:CompanyQuerySet.employers()(jobs/models.py:111-118), “wherever employers are counted or offered, this is the set”.merge_companiesends with_refuse_what_is_left(jobs/merging.py:230-250,:502), which asks Django what a delete would take along; a link set to null on delete is recorded by Django as an update, not a deletion, so a new link would be cleared silently unless the merge moves it. The delete dialog counts postings, applications and contacts only (jobs/views.py:574-610).src/postulo/api/routers/).Checked against
mainatf09b6010b.Proposal
Three commits, in this order; each leaves
maingreen.Experience.company, a nullable foreign key tojobs.Company(SET_NULL, so deleting a company never deletes a career entry), added by a migration that invents nothing.organisationstays, stays required, and stays what every theme, the text, the Word file, search and the candidate file read. In the form it remains a text box with the person's companies offered (as the capture form offers them); on save the text is matched to one of the owner's companies by name, case-insensitively, and linked, and where none matches the company is added (reusingget_or_create_company), marked as from the career (decision below): a boolean column onCompany, default false, added by the same migration, so it goes intoCOMPANY_FIELDS(core/export.py:164-176) andCompanyOut(api/schemas.py:87, read-only) in this commit, and the archive format goes up by one here for that alone. The form says so in one sentence. The page for an entry shows the company as a link; the company's page lists the entries that name it (“Your history here”)._movedline in the plan and the confirmation, with a test that proves the link survives); the delete dialog adds “N career entries keep their text and lose the link”. A company marked as from the career is left out of the posting, contact and agency pickers and the capture suggestions unless a row already names it, out of the map until it has a posting, and out of the companies table unless a filter asks for it; it stops being marked as soon as a posting, a contact or an application is attached, or by hand. A company merge keeps the mark only if both companies had it.company, the linked company's name or blank (core/export.pyRESUME_FIELDS); the importer pops it and links in a pass after the companies are restored, by name, and an older archive without the key restores unlinked. The candidate file carries the same field (one spelling of the block), read back as a hint: on the review page an entry whosecompanyexactly matches one of the account's companies is offered as linked; a file never creates a company (a stranger's file must not fill the list). The Europass import adds the same exact-name link and never adds a company. Both formats go up by one from whatever they are when this commit lands (FORMAT_VERSION,CANDIDATE_FORMAT, each with its history line and the newSHAPESfingerprint); the archive has already gone up once for the flag in the first commit.Decisions
translatable.py:16-27and the wiki's two paragraphs say an employer is free text so that former employers stay out of the company list. The maintainer now wants the organisation to be a company. Recommended: supersede the list argument and keep the protection it stood for, with the from-the-career marker below; keep the translation rule unchanged for now (next decision). The docstrings and the wiki (Your-career-record.md: the “kept separately” paragraph and “Some things are not translated, on purpose”) are corrected in the same commit.Company(default false, so every existing company is what it was), set when the career form adds the company and cleared by the first posting, contact or application. (a) puts every school-holiday employer into the posting picker; (c) leaves the maintainer's sentence half true. A derived rule (“no postings and named by an entry”) needs no column but hides a company the person typed on purpose and also worked for. The flag is inCOMPANY_FIELDSand inCompanyOutfrom the commit that adds it.Builds on
Issues #131 (career entries per language, the decision above), #138 (an employer is a tree), #202 and #239 (company kinds, the agency), #308 (what a CV prints), #181 (the candidate file), #671 (the picker's shape, not its rule).
Needed by
#685 and #686 reuse the field and the marker. #693 and #695 (organisations and providers) take the same field. #673 is where a company's name in a language comes from.
Done when
tests/test_career_companies.py: saving an experience links an existing company by name, adds one marked as from the career where there is none, and never links another owner's company (a name that exists only under another account adds a new one); the marker is cleared by a posting, a contact or an application; the pickers, the suggestions, the map and the table leave it out and a filter brings it back.tests/test_merging.py: a merge moves the entries and the confirmation says so;tests/test_jobs_isolation.pyandtests/security/test_isolation_sweep.pyshow another account's company is neither offered nor linked; a delete keeps the entry, its text and its translations.tests/test_career_languages.pykeeps passing unchanged (the organisation is still not translatable);tests/test_documents.pyandtests/test_document_formats.pyshow a CV, the text and the Word file print the same line as before;tests/test_help_text.pyfollows the field's help.tests/test_export.pyand the archive round trip carry the link at the new format and an older archive restores unlinked;tests/test_candidate_file.pyholds the newSHAPESline, the review row offers an exact match and a file never adds a company (tests/security/test_candidate_file.pywith a hostilecompany);tests/test_importers.pyfor the importer's pass;tests/test_europass_candidate.pyfor the exact-name link.tests/test_api.py). No search change, no new page. The processing record (core/gdpr.py) is drawn from plugins and connections and needs no line. The standing checklist otherwise applies: the archive format is one more than whatever it is when each commit lands (29 atf09b6010b; the flag and the link are two bumps), the candidate format likewise for the link (4), strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and in Weblate (#349);Your-career-record.mdand the docstrings are changed in the same commit.From the list of improvements of 2026-10-02.
Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#683).