From 8856c38b4d5a3bc12764ba6b525614b7feadf437 Mon Sep 17 00:00:00 2001 From: blindxfish Date: Mon, 17 Aug 2026 23:12:39 +0200 Subject: [PATCH] Show a post's image on its card, and show it full size _attachment_display.html iterates a context variable named `attachments`, so every caller binds it before the include. _post_card.html was the one caller that did not: it guarded on item.attachments but included the partial with nothing bound, so the gallery looped over whatever `attachments` happened to be in the surrounding page context and rendered empty. Every post with an image looked image-less on the feed and on profiles, and on a project page - where project_detail.html sets `attachments` at template scope for the project's own files - a devlog card would have rendered the project's files as its own. With the image actually reaching the card, render a lone one properly: a gallery holding exactly one item gets a `single` class and takes the full content column (max-height 480px, object-fit contain, no hover scale), matching the original DevPlace. That branch serves the stored original rather than thumbnail_url, because a thumbnail is 200px on its longest side and stretching it to the column width is visibly blurry. Animated GIFs needed no change and now have a test proving it: they never had a thumbnail to flatten, so they already took the original-file path and simply render larger. Co-Authored-By: Claude Opus 5 (1M context) --- devplacepy/static/css/attachments.css | 17 ++++ devplacepy/static/js/CLAUDE.md | 6 +- devplacepy/templates/_attachment_display.html | 9 +-- devplacepy/templates/_post_card.html | 3 +- tests/conftest.py | 17 ++++ tests/e2e/feed.py | 24 +++++- tests/e2e/post.py | 79 ++++++++++++++++++- 7 files changed, 146 insertions(+), 9 deletions(-) diff --git a/devplacepy/static/css/attachments.css b/devplacepy/static/css/attachments.css index 4a180982..3f7fcd9e 100644 --- a/devplacepy/static/css/attachments.css +++ b/devplacepy/static/css/attachments.css @@ -29,6 +29,23 @@ max-height: 200px; } +.attachment-gallery.single .attachment-gallery-item:has(.gallery-thumb) { + width: 100%; + max-width: 100%; +} + +.attachment-gallery.single .attachment-gallery-item:has(.gallery-thumb):hover { + transform: none; + border-color: var(--border-light); +} + +.attachment-gallery.single .gallery-thumb { + width: 100%; + max-width: 100%; + max-height: 480px; + object-fit: contain; +} + .attachment-gallery-item:has(.non-image) { width: 120px; height: 120px; diff --git a/devplacepy/static/js/CLAUDE.md b/devplacepy/static/js/CLAUDE.md index f130c745..07a15f07 100644 --- a/devplacepy/static/js/CLAUDE.md +++ b/devplacepy/static/js/CLAUDE.md @@ -92,7 +92,11 @@ File validation: max 5MB, allowed extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, ` **Ingesting a file from a URL.** `store_attachment_from_url(url, user_uid, filename=None)` (async, in `attachments.py`) is the remote counterpart to `store_attachment`: it downloads the URL on the server through `fetch_remote_file()` - SSRF-guarded (`_guard_public_url` resolves the host and refuses private/loopback/reserved/multicast addresses, mirroring the Devii fetch guard) and size-capped (streams, aborting once `_get_max_upload_bytes()` is exceeded) - resolves a filename from the URL path or the response `Content-Type` (`MIME_TO_EXT`), then calls `store_attachment()` so the bytes land in the **exact same** pipeline (validation, thumbnailing, DB row). It raises `RemoteFetchError(message, status)` which the route maps to an HTTP status. It is exposed at `POST /uploads/upload-url` (`UploadUrlForm{url, filename?}`, `require_user_api`) and as the Devii catalog action `attach_url` (handler `http`, `requires_auth=True`); both return the same record as `/uploads/upload`. The returned `uid` binds to a resource the same way as any upload - via `attachment_uids` at create/edit time - so attaching a remote image is just `attach_url` then `create_post`/`create_project`/etc. with that uid. Do not re-download remote files in a router; reuse this helper so the guard and size cap stay in one place. -`_row_to_attachment()` / `store_attachment()` expose `is_image` and `is_video` (derived from the mime prefix). The shared partial `templates/_attachment_display.html` branches image -> ``, video -> `