diff --git a/coulomb_social/apps/spaces/views.py b/coulomb_social/apps/spaces/views.py index 7433e55..6e52366 100644 --- a/coulomb_social/apps/spaces/views.py +++ b/coulomb_social/apps/spaces/views.py @@ -377,6 +377,14 @@ def page_edit(request: HttpRequest, slug: str, page_slug: str = "index") -> Http return redirect("spaces:detail", slug=slug) doc = pageops.read_page(space.slug, page_slug) + # Show files from the saved page even when a rejected edit redisplays the + # submitted fields. Attachment mutations keep their own revision checks. + try: + saved_files = attachments.page_files(space.slug, doc) if doc.exists else [] + attachment_error = "" + except (pageops.PageOpsError, OSError) as exc: + saved_files = [] + attachment_error = _mutation_error(exc) if request.method == "POST": title = (request.POST.get("title") or "").strip() abstractor = (request.POST.get("abstractor") or "").strip() @@ -421,6 +429,8 @@ def page_edit(request: HttpRequest, slug: str, page_slug: str = "index") -> Http "submitted_slug": request.POST.get("slug", page_slug), "is_new": not doc.exists, "form_title": "Edit or rename page" if doc.exists else "New page", + "saved_files": saved_files, + "attachment_error": attachment_error, }, ) diff --git a/coulomb_social/templates/spaces/page_form.html b/coulomb_social/templates/spaces/page_form.html index 349e8c5..526373b 100644 --- a/coulomb_social/templates/spaces/page_form.html +++ b/coulomb_social/templates/spaces/page_form.html @@ -32,6 +32,30 @@

{{ form_title }}

Change the title to rename the displayed protocol. Change the file name below to rename its Markdown file and page address.

+ {% if not is_new %} +
+

Attachments

+

Files on the saved page. Editing or renaming the page keeps its attachments.

+ {% if attachment_error %} +

{{ attachment_error }}

+ {% else %} + + {% endif %} + Manage attachments (new tab) +

Save your edits before adding or removing files, then reopen the editor.

+
+ {% endif %} +
{% csrf_token %} diff --git a/scripts/test-attachments-browser.mjs b/scripts/test-attachments-browser.mjs index ab7589d..7e26d55 100644 --- a/scripts/test-attachments-browser.mjs +++ b/scripts/test-attachments-browser.mjs @@ -32,9 +32,9 @@ try { ? 'Open, download or manage attachments' : 'Open or download attachments', exact: true}).click(); await page.waitForURL('**/attachments/'); } - async function downloadedBytes() { - const pending = page.waitForEvent('download'); - await page.getByRole('link', {name: 'Download', exact: true}).click(); + async function downloadedBytes(tab = page) { + const pending = tab.waitForEvent('download'); + await tab.getByRole('link', {name: 'Download', exact: true}).click(); const stream = await (await pending).createReadStream(); const chunks = []; for await (const chunk of stream) chunks.push(chunk); @@ -50,6 +50,35 @@ try { assert.match(await page.locator('main').innerText(), /Attachment added/); const originalDownload = await page.getByRole('link', {name: 'Download', exact: true}).getAttribute('href'); assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + const editUrl = `${shared}pages/notes/edit/`; + await page.goto(editUrl); + const files = page.getByRole('region', {name: 'Attachments', exact: true}); + assert.match(await files.innerText(), /meeting-notes.txt/); + assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + await page.getByLabel('Body (markdown)').fill('Unsaved author notes'); + const opened = context.waitForEvent('page'); + await page.getByRole('link', {name: 'Manage attachments (new tab)', exact: true}).click(); + const management = await opened; + await management.waitForURL('**/attachments/'); + assert.equal(await page.getByLabel('Body (markdown)').inputValue(), 'Unsaved author notes'); + assert.match(await management.locator('main').innerText(), /meeting-notes.txt/); + await management.close(); + const stale = await context.newPage(); + await stale.goto(editUrl); + assert.match(await stale.getByRole('region', {name: 'Attachments', exact: true}).innerText(), /meeting-notes.txt/); + await stale.getByLabel('Body (markdown)').fill('Keep my stale edit'); + await page.getByRole('button', {name: 'Save page', exact: true}).click(); + await page.waitForURL('**?page=notes'); + await stale.getByRole('button', {name: 'Save page', exact: true}).click(); + await stale.waitForURL('**/edit/'); + assert.match(await stale.locator('main').innerText(), /This page changed/); + assert.equal(await stale.getByLabel('Body (markdown)').inputValue(), 'Keep my stale edit'); + assert.match(await stale.getByRole('region', {name: 'Attachments', exact: true}).innerText(), /meeting-notes.txt/); + assert.equal(await downloadedBytes(stale), 'Synthetic attachment bytes'); + await stale.close(); + await page.goto(editUrl); + assert.equal(await page.getByLabel('Body (markdown)').inputValue(), 'Unsaved author notes\n'); + assert.match(await files.innerText(), /meeting-notes.txt/); await login('file-reader'); await menu(source, false); assert.equal(await page.getByRole('button', {name: 'Add attachment', exact: true}).count(), 0); diff --git a/tests/test_attachments.py b/tests/test_attachments.py index 5533436..4f9f270 100644 --- a/tests/test_attachments.py +++ b/tests/test_attachments.py @@ -151,7 +151,24 @@ def test_inline_body_files_have_authenticated_download_links(people, client): def test_edit_and_rename_keep_attachment_associations(people, client): space, doc, _, _, _ = people added = upload(space, doc) - response = client.post(reverse("spaces:page_edit", args=[space.slug, doc.slug]), { + edit_url = reverse("spaces:page_edit", args=[space.slug, doc.slug]) + response = client.get(edit_url) + assert response.status_code == 200 + assert len(response.context["saved_files"]) == 1 + download = response.context["saved_files"][0]["download_url"] + assert download.encode() in response.content + # Another tab changes the page after this editor opened. Redisplaying the + # rejected text must still expose the saved attachments without losing them. + stale = client.post(edit_url, { + "title": "Older edit", "slug": doc.slug, "body": "Unsaved text", + "revision": doc.revision, + }) + assert stale.status_code == 200 + assert b"This page changed" in stale.content + assert stale.context["doc"].body == "Unsaved text" + assert download.encode() in stale.content + assert pageops.read_page(space.slug, doc.slug).revision == added.revision + response = client.post(edit_url, { "title": "Renamed notes", "slug": "renamed", "body": "Updated notes", "revision": added.revision, })