From faca68fd3505d2ab2d92e132cde0b6de5f7714cf Mon Sep 17 00:00:00 2001 From: Simon Carstensen Date: Fri, 29 May 2026 11:25:15 +0200 Subject: [PATCH 1/2] fix: resolve spaced page slugs --- jottit/static/js/new-page.js | 3 ++- jottit/urls.py | 10 ++++++++++ jottit/views/page.py | 4 +++- tests/test_page_edit.py | 23 +++++++++++++++++++++++ tests/test_urls.py | 9 ++++++++- 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/jottit/static/js/new-page.js b/jottit/static/js/new-page.js index 438e9a5..58272bb 100644 --- a/jottit/static/js/new-page.js +++ b/jottit/static/js/new-page.js @@ -30,7 +30,8 @@ if (!value) return; e.preventDefault(); const base = form.getAttribute("action") || "/"; - const href = `${base}${encodeURIComponent(value)}?m=edit`; + const slug = value.toLowerCase().replaceAll(" ", "_"); + const href = `${base}${encodeURIComponent(slug)}?m=edit`; window.location.assign(href); }); })(); diff --git a/jottit/urls.py b/jottit/urls.py index b1e793e..830da91 100644 --- a/jottit/urls.py +++ b/jottit/urls.py @@ -10,6 +10,16 @@ def page_slug(name: str) -> str: return quote(name.lower().replace(" ", "_")) +def page_name_from_slug(slug: str) -> str: + """Best-effort page name from a URL slug. + + Page URLs are the lowercased page name with spaces written as + underscores. Flask has already percent-decoded the route segment by the + time views receive it. + """ + return slug.lower().replace("_", " ") + + def site_root() -> str: """URL path prefix that page names hang off for the current request. diff --git a/jottit/views/page.py b/jottit/views/page.py index f51fd20..7e9a656 100644 --- a/jottit/views/page.py +++ b/jottit/views/page.py @@ -21,7 +21,7 @@ ) from jottit.diff import better_diff from jottit.render import format_content -from jottit.urls import page_slug, site_root +from jottit.urls import page_name_from_slug, page_slug, site_root def home(site_slug: str) -> ResponseReturnValue: @@ -70,6 +70,8 @@ def view(site_slug: str, page_name: str) -> ResponseReturnValue: if mode == "edit" and not page_name and (new_name := request.args.get("name", "").strip()): return redirect(f"{site_root()}{page_slug(new_name)}?m=edit", code=303) + page_name = page_name_from_slug(page_name) + action = _action_for(mode) if (response := auth.gate(action)) is not None: return response diff --git a/tests/test_page_edit.py b/tests/test_page_edit.py index 834378f..afcacf7 100644 --- a/tests/test_page_edit.py +++ b/tests/test_page_edit.py @@ -149,6 +149,29 @@ def test_post_create_new_page_on_save(client: FlaskClient, db_engine: Engine) -> assert rev.revision == 1 +def test_post_create_page_with_space_redirects_to_readable_slug( + client: FlaskClient, db_engine: Engine +) -> None: + site_id = _seed_site(db_engine, secret_url="e7space", public_url="etaspace") + + response = client.post( + "/foo_bar", + base_url="http://etaspace.jottit.test/", + data={"content": "first version"}, + ) + + assert response.status_code == 303 + assert response.headers["Location"].endswith("/foo_bar") + + follow = client.get("/foo_bar", base_url="http://etaspace.jottit.test/") + assert follow.status_code == 200 + assert b"first version" in follow.data + + with db_engine.connect() as conn: + page = get_page(conn, site_id=site_id, page_name="foo bar") + assert page is not None + + def test_post_create_via_secret_url_redirects_under_prefix( client: FlaskClient, db_engine: Engine ) -> None: diff --git a/tests/test_urls.py b/tests/test_urls.py index 621aaa3..168380a 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -2,7 +2,7 @@ from flask import Flask -from jottit.urls import page_slug, site_root +from jottit.urls import page_name_from_slug, page_slug, site_root # ---- page_slug ---- @@ -19,6 +19,13 @@ def test_page_slug_empty_returns_empty() -> None: assert page_slug("") == "" +# ---- page_name_from_slug ---- + + +def test_page_name_from_slug_restores_spaces_and_lowercases() -> None: + assert page_name_from_slug("Foo_Bar") == "foo bar" + + # ---- site_root ---- From ed4b94ad7ffe044de7cdccdb87443b3c3f07894b Mon Sep 17 00:00:00 2001 From: Simon Carstensen Date: Fri, 29 May 2026 11:37:29 +0200 Subject: [PATCH 2/2] style: format page view tests --- tests/test_page_view.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_page_view.py b/tests/test_page_view.py index 55fb818..6fe612c 100644 --- a/tests/test_page_view.py +++ b/tests/test_page_view.py @@ -157,9 +157,7 @@ def test_wikilinks_resolve_against_subdomain_root(client: FlaskClient, db_engine # ---- home_layout=feed ---- -def test_home_renders_feed_when_home_layout_is_feed( - client: FlaskClient, db_engine: Engine -) -> None: +def test_home_renders_feed_when_home_layout_is_feed(client: FlaskClient, db_engine: Engine) -> None: site_id = _seed_site(db_engine, secret_url="fd1", public_url="feed-alpha", content="home body") with db_engine.begin() as conn: new_page(conn, site_id=site_id, name="post-one", content="**first post**") @@ -197,9 +195,7 @@ def test_home_feed_empty_state(client: FlaskClient, db_engine: Engine) -> None: assert "No pages yet" in response.data.decode() -def test_home_still_renders_page_in_default_mode( - client: FlaskClient, db_engine: Engine -) -> None: +def test_home_still_renders_page_in_default_mode(client: FlaskClient, db_engine: Engine) -> None: # Sanity-check the default branch survives the home_layout addition. _seed_site(db_engine, secret_url="fd3", public_url="feed-gamma", content="just a page")