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:
parent
6a4930ab15
commit
e43f17658a
7 changed files with 33 additions and 3 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = '\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",
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue