feat(booth): asks render INLINE in a verbatim report, placed by the author
Operator verdict on the separate /asks page: the question belongs with the artifact it is about. A four-voice audition wants each voice's radio group under that voice's audio, and one submit for the lot. - booth/inline.py: data-booth-ask="stem" | "stem:key" | data-booth-ask-submit, plus <!-- booth:ask ... --> comments; unknown stem left alone, not blanked - _ask_inline.html: self-contained fragments (own scoped styles, no JS), per-question groups bound to one form via the HTML5 form= attribute so a scattered multi-question ask still POSTs once - unplaced questions and a missing submit block are appended, so a partially marked-up page can never produce an unsubmittable 400 - chip becomes a jump link to the first open ask; /asks page kept as a fallback - 6 tests (one caught the partial-placement drop); v0.1.14
This commit is contained in:
+84
-9
@@ -86,6 +86,11 @@ from booth.asks import ( # noqa: E402
|
||||
valid_stem,
|
||||
write_answer,
|
||||
)
|
||||
from booth.inline import ( # noqa: E402
|
||||
form_id as ask_form_id,
|
||||
has_placeholders,
|
||||
place as place_asks,
|
||||
)
|
||||
from booth.links import ( # noqa: E402
|
||||
LINK_LOCK,
|
||||
LINKS_FILE,
|
||||
@@ -399,12 +404,13 @@ _BACK_CHIP = (
|
||||
# say so (found 2026-09-09 on `emmie-anchor`: valid ask, CLI listed it, page
|
||||
# showed nothing). Same injection mechanism as the back chip; it links to the
|
||||
# standalone /asks page, which renders the real forms.
|
||||
def asks_chip(name: str, open_count: int) -> str:
|
||||
def asks_chip(name: str, open_count: int, href: str | None = None) -> str:
|
||||
if open_count < 1:
|
||||
return ""
|
||||
label = f"? {open_count} open ask" + ("" if open_count == 1 else "s")
|
||||
href = href or f"/b/{quote(name, safe='')}/asks"
|
||||
return (
|
||||
f'<a href="/b/{quote(name, safe="")}/asks" class="booth-nav-asks">{label}</a>'
|
||||
f'<a href="{href}" class="booth-nav-asks">{label}</a>'
|
||||
"<style>.booth-nav-asks{position:fixed;top:0;right:7.2rem;z-index:2147483647;"
|
||||
"display:inline-block;margin:.6rem;padding:.34rem .72rem;"
|
||||
"font:700 13px/1.25 ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;"
|
||||
@@ -636,13 +642,12 @@ def create_app(
|
||||
# a pathological large file falls back to serving raw, unwrapped.
|
||||
try:
|
||||
if own_index.stat().st_size <= WRAP_MAX_BYTES:
|
||||
open_asks = sum(1 for a in list_asks(booth) if a["answer"] is None and not a["error"])
|
||||
return HTMLResponse(
|
||||
wrap_verbatim_html(
|
||||
own_index.read_text(encoding="utf-8", errors="replace"),
|
||||
extra=asks_chip(name, open_asks),
|
||||
)
|
||||
)
|
||||
raw = own_index.read_text(encoding="utf-8", errors="replace")
|
||||
# Asks render INLINE, where the report author put them (or
|
||||
# appended, if they marked nothing) — a question about an
|
||||
# artifact belongs beside that artifact, not on another page.
|
||||
body, tail = inject_asks(name, booth, raw)
|
||||
return HTMLResponse(wrap_verbatim_html(body, extra=tail))
|
||||
except OSError:
|
||||
pass
|
||||
return FileResponse(str(own_index), media_type="text/html")
|
||||
@@ -716,6 +721,76 @@ def create_app(
|
||||
base = f"/b/{quote(name, safe='')}/asks"
|
||||
return RedirectResponse(url=f"{base}#ask-{quote(ask, safe='')}", status_code=303)
|
||||
|
||||
_frag = templates.env.get_template("_ask_inline.html").module
|
||||
|
||||
def inject_asks(name: str, booth: Path, html: str) -> tuple[str, str]:
|
||||
"""(body, tail) for a verbatim booth: placeholders substituted in place,
|
||||
and whatever still has to be appended before </body>.
|
||||
|
||||
Marked-up pages get each fragment exactly where the author put it. An
|
||||
unmarked page gets the whole ask appended — an ask is NEVER invisible,
|
||||
which is the guarantee; markup only moves it somewhere better. A stem
|
||||
whose questions were placed but whose submit block was not gets that
|
||||
block appended, so a scattered form is always submittable.
|
||||
"""
|
||||
asks = list_asks(booth)
|
||||
if not asks:
|
||||
return html, ""
|
||||
url = quote(name, safe="")
|
||||
|
||||
seen: set[str] = set()
|
||||
|
||||
def render(kind: str, ask: dict, key: str | None) -> str:
|
||||
fid = ask_form_id(ask["stem"])
|
||||
if kind == "whole":
|
||||
frag = str(_frag.whole(ask, fid, url))
|
||||
elif kind == "submit":
|
||||
frag = str(_frag.submit(ask, fid, url))
|
||||
else:
|
||||
q = next(q for q in ask["questions"] if q.get("key") == key)
|
||||
frag = str(_frag.question(ask, q, fid, url))
|
||||
# An anchor on the FIRST fragment of each stem, wherever it landed,
|
||||
# so the floating chip can jump to it on a long report. Computed
|
||||
# here rather than in the macros because only the caller knows
|
||||
# which fragment came first.
|
||||
if ask["stem"] not in seen:
|
||||
seen.add(ask["stem"])
|
||||
frag = f'<a id="bk-ask-{ask["stem"]}-top"></a>' + frag
|
||||
return frag
|
||||
|
||||
tail = [str(_frag.styles())]
|
||||
if has_placeholders(html):
|
||||
html, placed, submitted = place_asks(html, asks, render)
|
||||
for a in asks:
|
||||
keys = placed.get(a["stem"])
|
||||
if keys is None:
|
||||
tail.append(render("whole", a, None)) # unmarked: never dropped
|
||||
continue
|
||||
if a["error"]:
|
||||
continue
|
||||
if None not in keys:
|
||||
# Partially marked up: append every question the author did
|
||||
# NOT place. A multi-question ask needs all of them or the
|
||||
# POST is a 400 — met only after the operator fills it in.
|
||||
for q in a["questions"]:
|
||||
if q.get("key") not in keys:
|
||||
tail.append(render("question", a, q.get("key")))
|
||||
if a["stem"] not in submitted:
|
||||
tail.append(render("submit", a, None)) # scattered but submittable
|
||||
else:
|
||||
for a in asks:
|
||||
tail.append(render("whole", a, None))
|
||||
|
||||
# The chip is now a JUMP LINK to the inline block, not a way out to a
|
||||
# separate page: on a long report the question can be well below the
|
||||
# fold, and "there is a question waiting" still has to be visible at
|
||||
# first paint.
|
||||
first_open = next((a for a in asks if a["answer"] is None and not a["error"]), None)
|
||||
open_n = sum(1 for a in asks if a["answer"] is None and not a["error"])
|
||||
if first_open is not None:
|
||||
tail.append(asks_chip(name, open_n, href=f'#bk-ask-{first_open["stem"]}-top'))
|
||||
return html, "".join(tail)
|
||||
|
||||
@app.get("/b/{name}/asks", response_class=HTMLResponse)
|
||||
def booth_asks_page(request: Request, name: str):
|
||||
"""The asks panel on its own page. Reachable from any booth, and the ONLY
|
||||
|
||||
Reference in New Issue
Block a user