Draw a map pin as its holder's logo or picture, where there is one (#914) #1057

Merged
tiagoagueda merged 1 commit from 914-picture-pins into main 2026-10-27 23:14:00 +00:00
Owner

Closes #914

A map pin whose holder has a picture is drawn as that picture.

  • A company's pin is its logo, rounded like its tile.
  • The person's own address is their picture, round.
  • Everything else keeps the dot: contacts, which have no picture field, and companies without a logo.
  • Initials tiles are not used as pins. The issue asked for this to be confirmed: a field of coloured letters reads worse than dots.

How

  • Server: each entry in drawn gains picture, the same stamped address the {% company_logo %} / {% avatar %} tags use. Both tags now get it from logo_url() / avatar_url(), which the map reuses.
  • Client: a picture pin is a PictureIcon, an L.DivIcon subclass, holding Basecoat's avatar (<span class="avatar rounded|rounded-full"><img alt="">).
    • The icon is built only when Leaflet draws the marker, so pins inside a cluster ask for nothing. Each picture is requested once its pin is on the map, never all of them as the page opens.
    • It is built with DOM nodes, never from markup.
    • An error listener on the canvas, in the capture phase, gives a marker whose picture fails to load its dot back with setIcon. The dot then stays for later redraws as well.
  • Accessibility: the marker keeps its title and alt, so the inner image stays decorative (alt=""). The target stays 24px: a 20px picture with a 2px ring.
  • Ring: the brand's colour, drawn as an outline because a forced-colours theme drops box-shadows. Under forced colours it is CanvasText.
  • Legal state (#941): a dissolved company's picture is greyscale with a grey ring; one in administration gets an amber ring. The title still names the state.
  • Clusters are unchanged.

Checked

  • Access: jobs:company_logo checks shared_scope(..., READ) and accounts:avatar checks access as before. Both are same-origin, so the CSP needs no change. Responses are private, max-age=86400 behind the ?v= stamp, so redraws hit the cache.
  • Tests:
    • tests/test_company_map.py: which entries carry which picture, and the guarantees of the fallback and the accessible name.
    • tests/e2e/test_map.py: a stored logo is drawn and loads, with no CSP breaches. A logo flag with nothing behind it (404) falls back to the dot.
  • No new strings.

🤖 Generated with Claude Code

Closes #914 A map pin whose holder has a picture is drawn as that picture. - **A company's pin is its logo**, rounded like its tile. - **The person's own address is their picture**, round. - **Everything else keeps the dot:** contacts, which have no picture field, and companies without a logo. - **Initials tiles are not used as pins.** The issue asked for this to be confirmed: a field of coloured letters reads worse than dots. ## How - **Server:** each entry in `drawn` gains `picture`, the same stamped address the `{% company_logo %}` / `{% avatar %}` tags use. Both tags now get it from `logo_url()` / `avatar_url()`, which the map reuses. - **Client:** a picture pin is a `PictureIcon`, an `L.DivIcon` subclass, holding Basecoat's `avatar` (`<span class="avatar rounded|rounded-full"><img alt="">`). - The icon is **built only when Leaflet draws the marker**, so pins inside a cluster ask for nothing. Each picture is requested once its pin is on the map, never all of them as the page opens. - It is built with DOM nodes, never from markup. - An `error` listener on the canvas, in the capture phase, gives a marker whose picture fails to load its dot back with `setIcon`. The dot then stays for later redraws as well. - **Accessibility:** the marker keeps its `title` and `alt`, so the inner image stays decorative (`alt=""`). The target stays 24px: a 20px picture with a 2px ring. - **Ring:** the brand's colour, drawn as an `outline` because a forced-colours theme drops box-shadows. Under forced colours it is `CanvasText`. - **Legal state (#941):** a dissolved company's picture is greyscale with a grey ring; one in administration gets an amber ring. The title still names the state. - **Clusters** are unchanged. ## Checked - **Access:** `jobs:company_logo` checks `shared_scope(..., READ)` and `accounts:avatar` checks access as before. Both are same-origin, so the CSP needs no change. Responses are `private, max-age=86400` behind the `?v=` stamp, so redraws hit the cache. - **Tests:** - `tests/test_company_map.py`: which entries carry which picture, and the guarantees of the fallback and the accessible name. - `tests/e2e/test_map.py`: a stored logo is drawn and loads, with no CSP breaches. A logo flag with nothing behind it (404) falls back to the dot. - No new strings. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Draw a map pin as its holder's logo or picture, where there is one (#914)
Some checks failed
CI / Browser tests (Chromium) (push) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (push) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (push) Has been cancelled
CI / Database tests on PostgreSQL (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
3c1f83b982
A company's pin is its logo, rounded as its tile is, and the person's own address is their
picture, round, each in Basecoat's avatar ringed in the brand's colour (an outline, which a
forced-colours theme keeps). A contact, a company without a logo, and an initials tile keep
the dot. The marker keeps its title and alt, so the picture inside is decorative; it is
built only when Leaflet draws the marker, so a picture is asked for once its pin leaves a
cluster; and one that fails to load gives its marker the dot back.

Closes #914

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed 914-picture-pins from 3c1f83b982
Some checks failed
CI / Browser tests (Chromium) (push) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (push) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (push) Has been cancelled
CI / Database tests on PostgreSQL (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
to 7a59a09532
Some checks failed
CI / Unit tests and coverage (Python 3.14) (push) Failing after 16m11s
CI / Database tests on PostgreSQL (push) Successful in 3m35s
CI / Browser tests (Chromium) (push) Successful in 33m12s
CI / Security audit (push) Successful in 1m0s
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m7s
Every Python / Unit tests (Python 3.12) (push) Failing after 12m18s
CI / Browser tests (Chromium) (pull_request) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
2026-10-27 15:13:23 +00:00
Compare
tiagoagueda force-pushed 914-picture-pins from 7a59a09532
Some checks failed
CI / Unit tests and coverage (Python 3.14) (push) Failing after 16m11s
CI / Database tests on PostgreSQL (push) Successful in 3m35s
CI / Browser tests (Chromium) (push) Successful in 33m12s
CI / Security audit (push) Successful in 1m0s
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m7s
Every Python / Unit tests (Python 3.12) (push) Failing after 12m18s
CI / Browser tests (Chromium) (pull_request) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
to 23a66c9405
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (push) Successful in 13m11s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m46s
CI / Database tests on PostgreSQL (push) Successful in 3m50s
CI / Security audit (push) Successful in 45s
CI / Browser tests (Chromium) (push) Successful in 31m45s
Every Python / Unit tests (Python 3.12) (push) Successful in 12m34s
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
2026-10-27 16:45:46 +00:00
Compare
tiagoagueda force-pushed 914-picture-pins from 23a66c9405
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (push) Successful in 13m11s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m46s
CI / Database tests on PostgreSQL (push) Successful in 3m50s
CI / Security audit (push) Successful in 45s
CI / Browser tests (Chromium) (push) Successful in 31m45s
Every Python / Unit tests (Python 3.12) (push) Successful in 12m34s
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
to 855b4d241a
Some checks failed
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m4s
CI / Unit tests and coverage (Python 3.14) (push) Successful in 14m40s
CI / Database tests on PostgreSQL (push) Successful in 4m17s
Every Python / Unit tests (Python 3.12) (push) Successful in 13m44s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m2s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m11s
CI / Security audit (push) Successful in 1m10s
CI / Browser tests (Chromium) (push) Successful in 34m48s
Every Python / Unit tests (Python 3.13) (push) Successful in 15m3s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m10s
CI / Security audit (pull_request) Successful in 1m9s
CI / Browser tests (Chromium) (pull_request) Failing after 45m40s
2026-10-27 23:11:35 +00:00
Compare
tiagoagueda deleted branch 914-picture-pins 2026-10-27 23:14:00 +00:00
Sign in to join this conversation.
No reviewers
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!1057
No description provided.