Export and import one kind of record as JSON: companies, contacts, listings and applications, each as a file of its own #659
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#659
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?
From the maintainer's list: “Preferable export/import format of ALL KIND data is JSON.” Postulo writes JSON for the whole account and for the candidate's own record, and serves it through the API, but nobody can take out “my companies” or “my contacts” as a file, and nothing reads such a file back. The whole-account archive is all or nothing, and its import only creates.
What exists today
src/postulo/core/export.py:80(FORMAT_VERSION = 29),build_document(:619), a zip with the files beside it. Its import,src/postulo/core/importer.py:406(load), is reached only bymanage.py import_data(core/management/commands/import_data.py): “Creates records; never merges”, and into an account that already holds a search only with--force, which makes a second copy of everything. Open defects: #354 (keys reach the model constructors), #375, #464, #466, #334.CANDIDATE_FORMAT = 4(core/export.py:104), written bybuild_candidate_document(:868) from the same builders as the archive; read byresume/candidate.pyasread(:353),plan(:1289) andapply(:1298) on a review page (resume/urls.py:13). Its stance (candidate.py:1-40): a review, not an action; it merges and never replaces; two entries are the same when they say the same thing, because ids in a file are local to it; every value goes through the form a person would have typed it in; the file is a stranger's, so its size (MAX_BYTES,:88) and rows (MAX_ROWS,:93) are bounded first.ContactExportView(jobs/views.py:818-841,core/gdpr.py:155), offered only when the data-protection notice is (#297): the answer to “what do you have on me?”.api/routers/), token-authenticated, with anupdated_sincecursor (#230). Its shapes are written by hand so that a new field never leaks (api/schemas.py:1-8). Contacts travel inside companies. There is no import there beyond creating one record.core/csv_import.py,core/views_import.py) is the other file import: upload, map and preview, import; duplicates reported and not created;MAX_BYTES2 MiB andMAX_ROWS5,000 (:37-38).jobs/models.py:313,get_or_create_companyinapplications/services.py:307);jobs/duplicates.pyfinds likely duplicates of a company (:222) and a contact (:270).Checked against
mainatf09b6010b.Proposal
This is the design and an order of work; each kind is a commit of its own after the shared part.
candidate_format:companies_format,contacts_format,listings_format,applications_format, each starting at 1, under the samepostuloheader (version,exported_at, anote). The archive's reader looks forformatand refuses these, as it refuses a candidate file (tests/test_candidate_file.py:425).Decisions
note, as the candidate file does of the picture.Prior art
No library is needed to read or write JSON. Considered for validation: pydantic 2.13.5 (MIT, 2026-08-28, already locked; the API's schemas are pydantic; OSV lists four advisories, all fixed before 2.4.0) and jsonschema 4.26.0 (MIT, 2026-01-07, no OSV advisory, not a dependency). The project's own habit, every value through the page's form, is stricter than either and already tested, so neither is adopted. Django's
serializerswould tie the file to model fields and primary keys, which the archive's own module says it is not (core/export.py:1-12): rejected. Hand-written precedent: the archive and candidate readers, in the standard library.Builds on / Needed by
Builds on #181, #334, #354, #375, #464, #466, #297 and #341. Needed by #660 and #661, which reuse the review page.
Done when
tests/test_kind_files.py(new): each kind's file round-trips between two accounts with nothing of the first in the second; a second import adds nothing; a block that changes shape is a new version of its file (the fingerprint test oftests/test_candidate_file.py:380); unknown keys, anowner_idand a foreign id write nothing; the archive's reader refuses each file.tests/security/test_kind_files.py(new) in the style oftests/security/test_candidate_file.py: size and row bounds, another account's records never in a file, a crafted file cannot name a record.tests/e2e/test_kind_files.py(new): export, upload, read the plan, confirm.tests/test_page_coverage.py,tests/security/test_isolation_sweep.pyand axe cover the new pages.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 (#659).