← back to Crazy News Channel
daily-cartoons/approve.py
84 lines
#!/usr/bin/env python3
"""Approve a queued cartoon INTO THE LOCAL SITE TREE ONLY. Never deploys/rsyncs.
python3 daily-cartoons/approve.py <slug> --date YYYY-MM-DD [--story-id <id>] [--section politics]
Copies poster/clip (clip is optional — poster-only cartoons render as a still image) to
cartoons/media/, writes cartoons/<date>-<slug>.html, and appends a manifest.js entry.
--story-id DEFAULTS to the article the cartoon was generated from (meta.json's story_id,
written by article-driven generate.py, TK-12237) and is still validated against
stories-data.js / real-news-data.js; pass --story-id to override. A story_id is REQUIRED
either way (standing rule TK-12158: every cartoon links to a real article).
Publishing to p24.agentabrams.com stays a separate, Steve-run deploy."""
import argparse, datetime, html, json, os, re, shutil, sys
import common as C
import build_review
CART = os.path.join(C.SITE, "cartoons")
TEMPLATE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cartoon-page.template.html")
def render_page(**fields):
"""Fill cartoon-page.template.html ({{key}} placeholders, single pass). Values must already be
HTML-escaped (media/source are pre-built markup)."""
tpl = open(TEMPLATE, encoding="utf-8").read().rstrip("\n")
return re.sub(r"\{\{(\w+)\}\}", lambda mo: fields[mo.group(1)], tpl)
def story_exists(sid):
for f in ("stories-data.js", "real-news-data.js"):
p = os.path.join(C.SITE, f)
if os.path.exists(p) and re.search(r"""["']?id["']?\s*:\s*["']%s["']""" % re.escape(sid), open(p).read()):
return True
return False
def main():
ap = argparse.ArgumentParser()
ap.add_argument("slug"); ap.add_argument("--date", default=datetime.date.today().isoformat())
ap.add_argument("--story-id", default=None); ap.add_argument("--section", default="politics")
a = ap.parse_args()
q = os.path.join(C.QUEUE, a.date, a.slug); mp = os.path.join(q, "meta.json")
if not os.path.exists(mp): sys.exit(f"not queued: {q}")
m = json.load(open(mp))
story_id = a.story_id or m.get("story_id")
if not story_id:
sys.exit("--story-id required (meta.json has no story_id — this cartoon predates the "
"article-driven pipeline; pass --story-id explicitly)")
if not story_exists(story_id): sys.exit(f"story_id '{story_id}' not found in stories-data.js / real-news-data.js")
a.story_id = story_id
base = f"{a.date.replace('-','')}-{a.slug}"
os.makedirs(os.path.join(CART, "media"), exist_ok=True)
shutil.copy2(os.path.join(q, "poster.jpg"), os.path.join(CART, "media", base + ".jpg"))
has_clip = os.path.exists(os.path.join(q, "clip.mp4"))
if has_clip: shutil.copy2(os.path.join(q, "clip.mp4"), os.path.join(CART, "media", base + ".mp4"))
e = html.escape
media = (f'<video controls playsinline poster="media/{base}.jpg" src="media/{base}.mp4"></video>' if has_clip
else f'<img src="media/{base}.jpg" alt="{e(m["title"])}">')
# TK-12226: link ONLY the shared stylesheet — no per-page <style> block,
# no locally hardcoded palette/font. Layout lives in assets/style.css
# under body.cartoon-page-simple so every future approved cartoon page
# renders in the site's one fixed palette + one font, same as every
# other page, with nothing that changes on click.
src_url, src_title = m.get("story_url"), m.get("story_title")
src_html = (f'<p class="src"><a href="{e(src_url)}" target="_blank" rel="noopener noreferrer">Source: {e(src_title or "linked article")}</a></p>'
if src_url else "")
# TK-12247: the page markup lives in daily-cartoons/cartoon-page.template.html, shared with the
# Inkwell Shadow Man export so every single-image cartoon page is rendered from ONE template.
open(os.path.join(CART, base + ".html"), "w").write(render_page(
title=e(m["title"]), media=media, caption=e(m["caption"]), source=src_html,
credit="AI-generated editorial cartoon in classic editorial-ink style. Invented figures only."))
mf = os.path.join(CART, "manifest.js"); src = open(mf).read()
mm = re.search(r"(window\.P24_CARTOONS\s*=\s*)(\[[\s\S]*\])(\s*;\s*)$", src)
arr = json.loads(mm.group(2)); arr = [x for x in arr if x.get("id") != a.slug]
arr.append({"id": a.slug, "title": m["title"], "file": base + ".html", "created_at": m["created_at"],
"category": "Political Cartoon", "section": a.section, "blurb": m["caption"],
"thumb": f"media/{base}.jpg", "story_id": a.story_id, "ai_generated": True,
"style_reference": "editorial-ink", "tags": ["editorial cartoon", "classic editorial ink", "daily cartoon"]})
open(mf, "w").write(src[:mm.start(2)] + json.dumps(arr, indent=2) + mm.group(3))
m["status"] = "approved"; m["approved_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
m["site_file"] = f"cartoons/{base}.html"; json.dump(m, open(mp, "w"), indent=2)
build_review.build()
print(f"approved locally → cartoons/{base}.html (+ manifest.js). NOT deployed.")
if __name__ == "__main__":
main()