Import one card from a chosen address book as a new company, into an existing company, or as a contact #9

Open
opened 2026-09-30 20:30:03 +00:00 by tiagoagueda · 1 comment
Owner

The only way a card reaches Postulo today is adoption: each card with a name and no link in the configured address book becomes a new contact, under the company its ORG names or a (no company) holding company (src/postulo_dav/sync.py:527-550). Nobody chooses the card, nothing is matched against existing contacts, and a company's card becomes a contact named after the company. The ask:

import from CardDAV to Postulo is an option, for instance to import a CardDAV contact as a new company (or update existing one).

Read here as a one-off copy of one card, started by the person, into a new company, an existing company, or a contact. Decisions 1 and 5 give the alternatives.

What the plugin should do

  • From any address book the person chose with read access (#13), one card at a time.
  • Into a new company; an existing company, updated; or, by the same route, a new contact.
  • Matching an existing company. Offer what get_or_create_company would match: the same name, ignoring case (src/postulo/applications/services.py in the core; a card has no Wikidata id). The person confirms or picks another; nothing merges silently. That call creates when nothing matches, so offering a match needs companies readable (postulo/postulo#328). Whether the choosing screen also uses the core's duplicate finder (src/postulo/jobs/duplicates.py: legal forms set aside, website domains) is for postulo/postulo#327.
  • What lands on a company:
    • FN of a KIND:org or X-ABShowAs:COMPANY card, otherwise the first ORG component → name;
    • URL → website; NOTE → notes;
    • ADR → the free-text location, locality and country only (a company is placed at city level); postal addresses once postulo/postulo#329 exists;
    • TEL and EMAIL → the company's numbers and email once postulo/postulo#329 lands; before it, decision 3;
    • LOGO or PHOTO → decision 4.
  • What lands on a contact: what adoption takes today (name, role, one email, the primary number, one link, notes), plus the ORG unit as that company's department and ADR as postal addresses. A card with no ORG gets no company (Contact.company is nullable now) rather than the holding company (sync.py:80).
  • Written through the core's services, with the plugin as the actor. Adoption today calls Contact.objects.create and save() itself (sync.py:537, :154), against Writing a plugin's "Never write by saving a model".
  • An import alone writes nothing to the server.

Decisions

  1. What "import" means. The alternatives to one card, once: a whole address book, which postulo/postulo#16 ruled out ("a family into a job tracker"); or an import that stays in step. Recommended: one card at a time, with "and keep in step" offered beside it, which makes it a pairing (#8): the rest of the plugin syncs, and a copy that quietly drifts would surprise.
  2. Updating a company: fill blanks or overwrite. The core's career-file importer only fills blanks (src/postulo/resume/importing.py); a merge keeps the kept record's value and shows the other (src/postulo/jobs/merging.py). Recommended: fill blanks, and list where card and company differ, for the person to take the card's value field by field.
  3. TEL and EMAIL before postulo/postulo#329. Into the company's notes as text, or dropped. Recommended: notes, under a line naming the card, so nothing is lost silently.
  4. The logo. A LOGO or PHOTO is inline data or an address. The core decodes, checks and re-encodes every logo it keeps (src/postulo/jobs/logos.py, off the surface). Recommended: inline data only, handed to the core if postulo/postulo#328 offers a call; no fetching an address during an import; until then, no logo.
  5. Other targets. "For instance" leaves room for more: an existing contact, a department, the person's own profile. Recommended: an existing contact too, matched and filled as a company is; a department only as a contact's ORG unit; not the person's own profile, which #8 keeps Postulo → card.

Needs from the core

  • postulo/postulo#327: the place to see a book's cards and "create a record from" one. A plugin cannot draw a page (postulo/postulo#228).
  • postulo/postulo#328: reading companies to match against, updating a company's fields, a department by name, and creating or updating a contact with its postal addresses. Today there is only get_or_create_company, by name or Wikidata id, setting nothing else.
  • postulo/postulo#329: numbers, email and addresses for a company.

Before they land nothing can be imported card by card: adoption in the configured book is all there is. Depends on #13.

Done when

  • From a book the person chose, one card becomes a new company with its name, website, notes and locality; another updates an existing company after the person confirms the match, filling only its blanks and listing the differences.
  • A card with ORG:Acme;Legal imported as a contact is a contact at Acme in its Legal department (with employer_structure on); a card with no ORG, a contact with no company.
  • Nothing else from a personal book reaches Postulo, and an import without "keep in step" writes nothing to the server.
  • New strings go through uv run postulo-messages extract, fr-fr and pt-pt filled on the commit (the test gates them), the 23 EU catalogues before a tag (uv run pytest -m release). New surface names mean repinning the core (pyproject.toml's dev group pins 1d6cdd7e0) and extending tests/test_surface.py.

From the list of DAV plugin improvements of 2026-09-30.

The only way a card reaches Postulo today is adoption: each card with a name and no link in the configured address book becomes a new contact, under the company its `ORG` names or a *(no company)* holding company (`src/postulo_dav/sync.py:527-550`). Nobody chooses the card, nothing is matched against existing contacts, and a company's card becomes a contact named after the company. The ask: > import from CardDAV to Postulo is an option, for instance to import a CardDAV contact as a new company (or update existing one). Read here as a one-off copy of one card, started by the person, into a new company, an existing company, or a contact. Decisions 1 and 5 give the alternatives. ## What the plugin should do - **From** any address book the person chose with read access (#13), one card at a time. - **Into** a new company; an existing company, updated; or, by the same route, a new contact. - **Matching an existing company.** Offer what `get_or_create_company` would match: the same name, ignoring case (`src/postulo/applications/services.py` in the core; a card has no Wikidata id). The person confirms or picks another; nothing merges silently. That call creates when nothing matches, so offering a match needs companies readable (postulo/postulo#328). Whether the choosing screen also uses the core's duplicate finder (`src/postulo/jobs/duplicates.py`: legal forms set aside, website domains) is for postulo/postulo#327. - **What lands on a company:** - `FN` of a `KIND:org` or `X-ABShowAs:COMPANY` card, otherwise the first `ORG` component → name; - `URL` → website; `NOTE` → notes; - `ADR` → the free-text location, locality and country only (a company is placed at city level); postal addresses once postulo/postulo#329 exists; - `TEL` and `EMAIL` → the company's numbers and email once postulo/postulo#329 lands; before it, decision 3; - `LOGO` or `PHOTO` → decision 4. - **What lands on a contact:** what adoption takes today (name, role, one email, the primary number, one link, notes), plus the `ORG` unit as that company's department and `ADR` as postal addresses. A card with no `ORG` gets no company (`Contact.company` is nullable now) rather than the holding company (`sync.py:80`). - **Written through the core's services, with the plugin as the actor.** Adoption today calls `Contact.objects.create` and `save()` itself (`sync.py:537`, `:154`), against *Writing a plugin*'s "Never write by saving a model". - **An import alone writes nothing to the server.** ## Decisions 1. **What "import" means.** The alternatives to one card, once: a whole address book, which postulo/postulo#16 ruled out ("a family into a job tracker"); or an import that stays in step. **Recommended:** one card at a time, with "and keep in step" offered beside it, which makes it a pairing (#8): the rest of the plugin syncs, and a copy that quietly drifts would surprise. 2. **Updating a company: fill blanks or overwrite.** The core's career-file importer only fills blanks (`src/postulo/resume/importing.py`); a merge keeps the kept record's value and shows the other (`src/postulo/jobs/merging.py`). **Recommended:** fill blanks, and list where card and company differ, for the person to take the card's value field by field. 3. **`TEL` and `EMAIL` before postulo/postulo#329.** Into the company's notes as text, or dropped. **Recommended:** notes, under a line naming the card, so nothing is lost silently. 4. **The logo.** A `LOGO` or `PHOTO` is inline data or an address. The core decodes, checks and re-encodes every logo it keeps (`src/postulo/jobs/logos.py`, off the surface). **Recommended:** inline data only, handed to the core if postulo/postulo#328 offers a call; no fetching an address during an import; until then, no logo. 5. **Other targets.** "For instance" leaves room for more: an existing contact, a department, the person's own profile. **Recommended:** an existing contact too, matched and filled as a company is; a department only as a contact's `ORG` unit; not the person's own profile, which #8 keeps Postulo → card. ## Needs from the core - postulo/postulo#327: the place to see a book's cards and "create a record from" one. A plugin cannot draw a page (postulo/postulo#228). - postulo/postulo#328: reading companies to match against, updating a company's fields, a department by name, and creating or updating a contact with its postal addresses. Today there is only `get_or_create_company`, by name or Wikidata id, setting nothing else. - postulo/postulo#329: numbers, email and addresses for a company. Before they land nothing can be imported card by card: adoption in the configured book is all there is. Depends on #13. ## Done when - From a book the person chose, one card becomes a new company with its name, website, notes and locality; another updates an existing company after the person confirms the match, filling only its blanks and listing the differences. - A card with `ORG:Acme;Legal` imported as a contact is a contact at Acme in its Legal department (with `employer_structure` on); a card with no `ORG`, a contact with no company. - Nothing else from a personal book reaches Postulo, and an import without "keep in step" writes nothing to the server. - New strings go through `uv run postulo-messages extract`, fr-fr and pt-pt filled on the commit (the test gates them), the 23 EU catalogues before a tag (`uv run pytest -m release`). New surface names mean repinning the core (`pyproject.toml`'s dev group pins `1d6cdd7e0`) and extending `tests/test_surface.py`. From the list of DAV plugin improvements of 2026-09-30.
Author
Owner

The maintainer's list of 2026-10-02 asks for vCard 4 as the import and export format for contact details. Core issue postulo/postulo#660 covers the reading side with a .vcf import (vCard 3.0 and 4.0), through one mapping offered on the plugin surface.

Reusable here: reading a card into plain values, KIND:org to tell a company from a contact, ORG:Company;Unit for a department, ADR checked against its country's rules (postulo/postulo#306), TEL through postulo/postulo#304, and the rule that an address in PHOTO or LOGO is never fetched (decision 4 here).

This issue keeps its own scope: one card at a time from a chosen address book, matching and updating a company, writing through the core's services, and “keep in step”.

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

The maintainer's list of 2026-10-02 asks for vCard 4 as the import and export format for contact details. Core issue postulo/postulo#660 covers the reading side with a `.vcf` import (vCard 3.0 and 4.0), through one mapping offered on the plugin surface. Reusable here: reading a card into plain values, `KIND:org` to tell a company from a contact, `ORG:Company;Unit` for a department, `ADR` checked against its country's rules (postulo/postulo#306), `TEL` through postulo/postulo#304, and the rule that an address in `PHOTO` or `LOGO` is never fetched (decision 4 here). This issue keeps its own scope: one card at a time from a chosen address book, matching and updating a company, writing through the core's services, and “keep in step”. From the list of improvements of 2026-10-02.
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-dav#9
No description provided.