← back to Crazy News Channel Shadowman
daily-cartoons/approve.py
76 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")
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 "")
open(os.path.join(CART, base + ".html"), "w").write(f'''<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>{e(m["title"])}</title>
<link rel="stylesheet" href="../assets/style.css"></head>
<body class="cartoon-page-simple"><p><a href="index.html">← Cartoon Desk</a></p>{media}<h1>{e(m["title"])}</h1>
<p class="cap">“{e(m["caption"])}”</p>{src_html}<p><small>AI-generated editorial cartoon in classic editorial-ink style. Invented figures only.</small></p></body></html>''')
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()