#!/usr/bin/env python3 """Offline private rendering cards and lossiness report; never grants acceptance.""" import argparse import hashlib import json import os import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # The standalone tool uses an integration-disabled, in-memory configuration. os.environ["DJANGO_SETTINGS_MODULE"] = "coulomb_social.settings.test" import django # noqa: E402 django.setup() from coulomb_social.apps.spaces.bubble_transfer import _records # noqa: E402 from coulomb_social.apps.spaces.rendering_audit import ( # noqa: E402 audit_body, review_card, review_index, ) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("articles", type=Path, help="Private captured articles.json") parser.add_argument("--output", type=Path, required=True, help="New private directory") args = parser.parse_args() try: output = args.output.expanduser().resolve() source = args.articles.expanduser().resolve() repo = Path(__file__).resolve().parents[1] if output.is_relative_to(repo) or source.is_relative_to(output): raise ValueError("Unsafe output location") captured = source.read_bytes() records = _records(json.loads(captured)) report = {"schema": "coulomb.bubble-rendering-audit.v1", "source_sha256": hashlib.sha256(captured).hexdigest(), "original_bubble_render_compared": False, "productive_fidelity_accepted": False, "records": []} cards = {} for identity, article in sorted(records.items()): body = article.get("content_text") title = str(article.get("nomer_text") or identity) if not isinstance(body, str): report["records"].append({"id": identity, "title": title, "status": "body_missing"}) continue audit, rendered = audit_body(body) card = hashlib.sha256(identity.encode()).hexdigest() + ".html" cards[card] = review_card(title, body, rendered, audit, source_id=identity) report["records"].append({"id": identity, "title": title, "status": "rendered", "card": card, "card_sha256": hashlib.sha256(cards[card]).hexdigest(), **audit}) output.mkdir(mode=0o700) for name, data in sorted({**cards, "index.html": review_index(report["records"])}.items()): fd = os.open(output / name, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "wb") as stream: stream.write(data) report["status"] = "completed_offline_rendering_audit" fd = os.open(output / "audit.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as stream: json.dump(report, stream, indent=2, sort_keys=True) stream.write("\n") print(json.dumps({ "records": len(records), "rendered_bodies": len(cards), "sanitization_changed_html": sum(r.get("sanitization_changed_html", False) for r in report["records"]), "markup_changed_by_sanitization": sum(r.get("markup_changed_by_sanitization", False) for r in report["records"]), "serialization_only_changes": sum(r.get("serialization_only_change", False) for r in report["records"]), "productive_fidelity_accepted": False, })) except (ValueError, OSError, TypeError): print("Rendering audit refused; private files retained. " "Without audit.json output is incomplete.", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())