post.html header prose branches on viewer-vs-owner: invitees see "shared with me, @viewer the {title}" + "created by @owner the {title}" instead of the owner-centric "just me / shared between" lines; owner view unchanged — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

- billboard.views.view_post adds viewer_is_owner + other_recipients context vars. is_real_invitee = (auth AND post has owner AND viewer != owner). Anon viewers + ownerless-post legacy path fall through to owner-style rendering (which renders empty gracefully via the at_handle / display_name AnonymousUser guards).
  - other_recipients = post.shared_with.exclude(viewer) when invitee; .all() otherwise.
  - post.html .post-header branches:
    • viewer_is_owner: existing prose ("just me, @owner …" / "shared between {recipients} & me, @owner …").
    • sole invitee: "shared with me, @viewer the {viewer.title}" + "created by @owner the {owner.title}".
    • multi invitee: "shared with {other_recipients}" + "& me, @viewer the {viewer.title}" + "created by @owner the {owner.title}".
  - lyric_extras at_handle + display_name: guard against AnonymousUser (no .email attribute) — return "" rather than crash. Preserves the Percival ch. 18 anon-views-ownerless-post path.
  - 12 new ITs in test_post_invitee_view (context vars: viewer_is_owner, other_recipients exclude/include; template prose: sole + multi invitee phrasing, owner unchanged).
  - 878 IT regression + 8 post-html FT regression green (1 Marionette flake on multi-run that passes in isolation).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-09 01:14:11 -04:00
parent 419e022140
commit 22d0507c3f
4 changed files with 199 additions and 7 deletions

View File

@@ -43,7 +43,11 @@ def relative_ts(dt):
@register.filter
def display_name(user):
if user is None:
# `getattr` guards: AnonymousUser has no `.email` attribute and is not
# None, so `if user is None` doesn't catch it. Render anonymous viewers
# as empty rather than crashing — the legacy ownerless-post path
# (Percival ch. 18) shows post detail to anon visitors.
if user is None or not getattr(user, "is_authenticated", False):
return ""
if user.username:
return user.username
@@ -55,8 +59,9 @@ def at_handle(user):
"""`@username` when the user has set one; falls back to the truncated
email otherwise (no `@` prefix on bare emails since the address itself
already carries the `@`). Used in post.html to colour usernames in the
--quaUser palette key while leaving emails as-is."""
if user is None:
--quaUser palette key while leaving emails as-is. Anonymous viewers
render as empty (legacy ownerless-post path)."""
if user is None or not getattr(user, "is_authenticated", False):
return ""
if user.username:
return f"@{user.username}"