Render private space covers in Spaces list entries

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a11538-05ea-7b63-b3c7-a2df5f38898d
This commit is contained in:
tegwick 2026-10-07 13:47:12 +02:00
parent 6a4930ab15
commit e43f17658a
7 changed files with 33 additions and 3 deletions

View file

@ -2,6 +2,7 @@ from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import redirect, render
from coulomb_social.apps.spaces.assets import local_asset_url
from coulomb_social.apps.spaces.services import spaces_for_member
from .principal import build_principal
@ -23,6 +24,8 @@ def app_home(request: HttpRequest) -> HttpResponse:
member = principal.get("member")
spaces = list(spaces_for_member(member))
for space in spaces:
space.visual_url = local_asset_url(space.slug, space.visual)
return render(
request,
"core/app_home.html",

View file

@ -17,13 +17,17 @@
{% for space in spaces %}
<li class="card" style="margin-top:0.75rem;">
<a href="{% url 'spaces:detail' space.slug %}" style="color:inherit;text-decoration:none;">
{% if space.visual_url %}
<img class="space-cover" src="{{ space.visual_url }}" alt="{{ space.title }} cover"
style="display:block;max-width:100%;max-height:8rem;object-fit:cover;border-radius:6px;margin-bottom:0.5rem;">
{% endif %}
<strong>{{ space.title }}</strong>
<span class="muted"> · {{ space.slug }}</span>
</a>
{% if space.description %}
<p class="muted" style="margin:0.35rem 0 0;font-size:0.95rem;">{{ space.description }}</p>
{% endif %}
{% if space.visual %}
{% if space.visual and not space.visual_url %}
<p class="muted" style="margin:0.25rem 0 0;font-size:0.85rem;">Visual: {{ space.visual }}</p>
{% endif %}
</li>

View file

@ -23,7 +23,8 @@
"tests/test_reichelag_workflow.py::test_full_protocol_duplicate_edit_and_clipboard_control",
"tests/test_reichelag_workflow.py::test_protocol_naming_rename_and_collision_safety",
"tests/test_private_assets.py::test_owner_and_reader_get_exact_private_image",
"tests/test_private_assets.py::test_outsider_foreign_tenant_revoked_archived_and_anonymous_denied"
"tests/test_private_assets.py::test_outsider_foreign_tenant_revoked_archived_and_anonymous_denied",
"tests/test_private_assets.py::test_display_uses_private_images_without_clipboard_changes"
],
"browser": [
"tests/usecases/test_reichelag_browser.py::test_weekly_call_through_real_ui"

View file

@ -50,6 +50,7 @@ try {
}
await login('writer');
assert.match(await page.locator('main').innerText(), new RegExp(scenario.space_title));
await page.waitForFunction(() => document.querySelector('.space-list .space-cover')?.naturalWidth === 1);
await page.locator(`a[href="/app/spaces/${scenario.space_slug}/"]`).click();
await page.getByRole('link', {name: scenario.previous_title, exact: true}).click();
await page.waitForFunction(() => document.querySelector('#page-content img')?.naturalWidth === 1);
@ -124,6 +125,7 @@ try {
assert.match(await page.locator('#page-content').innerText(), /Completed point/);
assert.doesNotMatch(await page.locator('#page-content').innerText(), /New notes/);
await login('reader');
await page.waitForFunction(() => document.querySelector('.space-list .space-cover')?.naturalWidth === 1);
await page.goto(`${detail}?page=${scenario.next_slug}`);
assert.match(await page.locator('#page-content').innerText(), /New notes/);
await page.waitForFunction(() => document.querySelector('#page-content img')?.naturalWidth === 1);
@ -143,6 +145,7 @@ try {
const denied = await page.goto(`${detail}pages/${scenario.next_slug}/edit/`);
assert.equal(denied.status(), 403);
await login('outsider');
assert.equal(await page.locator('.space-list .space-cover').count(), 0);
const forbidden = await page.goto(`${detail}?page=${scenario.next_slug}`);
assert.equal(forbidden.status(), 404);
assert.equal(await page.locator('#page-content').count(), 0);

View file

@ -135,6 +135,7 @@ def test_nested_raster_allowed_but_html_disguised_as_png_denied(images, client):
@pytest.mark.django_db
@pytest.mark.usecase("CSOC-UC-008")
def test_display_uses_private_images_without_clipboard_changes(images, client):
space, owner, _, _ = images
body = '![Body visual](../assets/cover.png)\n\n[Open visual](assets/cover.png)\n'
@ -149,6 +150,14 @@ def test_display_uses_private_images_without_clipboard_changes(images, client):
response = client.get(reverse("spaces:detail", kwargs={"slug": space.slug}) + "?page=notes")
assert b'alt="Pictures cover"' in response.content
assert b'alt="Notes visual"' in response.content
response = client.get(reverse("core:app_home"))
assert b'class="space-cover"' in response.content
assert f'src="{image_url(space)}"'.encode() in response.content
assert b"Visual: assets/cover.png" not in response.content
client.force_login(images[3].user)
response = client.get(reverse("core:app_home"))
assert b'class="space-cover"' not in response.content
assert image_url(space).encode() not in response.content
@pytest.mark.parametrize("reference", ["https://remote.test/image.png", "//remote.test/a.png",

View file

@ -36,7 +36,7 @@ def test_weekly_call_through_real_ui(tmp_path, settings, request):
user_engine_user_id=subject, issuer="https://local.dev/issuer", subject=subject,
)
space = create_space(actors["writer"], title=scenario["space_title"],
slug=scenario["space_slug"])
slug=scenario["space_slug"], visual=scenario["visual"])
pageops.assets_dir(space.slug).joinpath("cover.png").write_bytes(
base64.b64decode(scenario["visual_base64"])
)

View file

@ -585,3 +585,13 @@ revision/concurrency, cross-space body/incoming links and broader fidelity remai
T09 still awaits founder Visual and complete workflow acceptance; user asked to
refresh ReichelAG. No task/workplan completion or Bubble retirement inferred.
Receipt: docs/release/private-visuals-2026-10-07.md.
## User Visual review: missing Spaces-list preview — 2026-10-07
Founder confirms the Visual appears on the page, but not in Spaces-list entries.
T05/T09 retain this concrete review gap. Add the space cover to its existing
list entry using the same private route and membership-filtered query. Extend
UC-008 fast and real-browser checks for writer/viewer list-preview loading and
outsider exclusion. Preserve page/clipboard/content/access behavior. This is a
correction within the approved private-Visual image-only upgrade scope, not
acceptance of the complete use case. No wider UI framework work included.