Company identifiers should match regardless of letter case #211
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
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#211
Loading…
Add table
Add a link
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?
Company identifiers should match regardless of letter case.
q95andQ95,Aperture-Scienceandaperture-science, or a staff number typedab-12andAB-12should be one identifier: found by the same lookups, refused as the same duplicate, and never allowed onto two companies.How matching works today
Every comparison is an exact
value=match on the stored string:Company.by_identifier(jobs/models.py:331), used by the CSV import (core/csv_import.py:787), the importer (core/importer.py:511) and the Wikidata link on capture (applications/services.py:193);jobs/forms.py:309-332): already listed and already carries this identifier;jobs/services.py:40-90, which sets identifiers from elsewhere: theseenset, the clash with another company, andget_or_create;CompanyIdentifier(jobs/models.py:374-389), which PostgreSQL and SQLite both compare case-sensitively.Where that already works, and where it does not
Named schemes are safe for new values.
Scheme.normalise(core/identifiers.py:95) folds case before anything is stored or looked up:Other is not folded at all. The Other scheme has neither
uppernorlower, so:seen_values), the service'sseenset, andunique_other_identifier_per_company;scheme != identifiers.OTHER) and byone_company_per_identifier_per_owner. That part may be deliberate, since two companies can share a staff-number format, but it should be stated either way.Rows saved before folding existed. No migration ever re-normalised stored
CompanyIdentifiervalues. A row saved before a scheme gainedupper/lower(for example a lowercaseq95from an early Wikidata import, or a mixed-case LinkedIn slug) is invisible to every exact lookup above, and a correctly folded value can be added beside it on another company.What a fix has to settle
AB-12should still readAB-12. That means a case-insensitive comparison (value__iexact, or aLower("value")expression in the constraints) rather than folding the stored value.UniqueConstraints with expression constraints onLower("value")(Django supportsUniqueConstraint(Lower("value"), "scheme", …)). Check that the SQLite and PostgreSQL migrations both build.identifiers.clean. It must report, not silently merge, any pair that becomes a duplicate once folded: two companies may turn out to be one employer, and choosing which to keep is the person's call.by_identifier, the formset, andjobs/services.pycompare the same way as the constraint, so the form refuses with a readable message before the database raisesIntegrityError. This matches whatCompanyForm.clean_namealready does for names withname__iexact(jobs/forms.py:253-268).Lower()in PostgreSQL follows the database locale, and in SQLite it folds ASCII only, so a test with a non-ASCII Other value should pin down what is promised.PersonIdentifier(accounts/models.py:152) has the same shape. This issue is about companies; decide whether it follows and file it separately.Tests
Q95to one company andq95to another is refused, through the company form and throughjobs/services.py.AB-12andab-12on the same company is refused, and displays as first typed.Company.by_identifier(owner, "linkedin", "Aperture-Science")finds a company stored asaperture-science.