mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-07 16:18:54 +00:00
Fail closed on whitespace-only Google configuration and centralize shared authentication behavior.
195 lines
6.2 KiB
Python
195 lines
6.2 KiB
Python
"""
|
|
Generates a self-contained HTML order form from the latest scraped menu.
|
|
|
|
The form shows this week's meals with quantity selectors, a running total,
|
|
and submits orders to the backend (local Flask or cloud API Gateway).
|
|
|
|
Usage:
|
|
python3 generate_form.py # local mode
|
|
python3 generate_form.py --api-url URL --google-client-id ID # cloud mode
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
OUTPUT_DIR = PROJECT_ROOT / "output"
|
|
CONFIG_PATH = PROJECT_ROOT / "config.json"
|
|
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
|
|
|
|
|
def load_config():
|
|
with open(CONFIG_PATH) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def latest_menu() -> dict:
|
|
files = sorted(OUTPUT_DIR.glob("menu-*.json"), reverse=True)
|
|
if not files:
|
|
print("Error: No menu JSON found. Run scrape_menu.py first.", file=sys.stderr)
|
|
sys.exit(1)
|
|
with open(files[0]) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def _json_for_script(value) -> str:
|
|
"""Serialize for embedding in a <script> tag; escape < and JS line separators."""
|
|
return (
|
|
json.dumps(value, separators=(",", ":"))
|
|
.replace("<", "\\u003c")
|
|
.replace("\u2028", "\\u2028")
|
|
.replace("\u2029", "\\u2029")
|
|
)
|
|
|
|
|
|
def generate_form(
|
|
menu: dict,
|
|
config: dict,
|
|
api_url: str = "",
|
|
bulk_discount: float = 0,
|
|
company_subsidy: float = 0,
|
|
google_client_id: str = "",
|
|
) -> str:
|
|
google_client_id = google_client_id.strip()
|
|
if api_url and not google_client_id:
|
|
raise ValueError("Google client ID is required in cloud mode")
|
|
|
|
week = datetime.now().strftime("%Y-W%U")
|
|
scraped_at = menu.get("scraped_at", "unknown")
|
|
deadline = config.get("order_deadline", "Thursday 11:59 PM")
|
|
has_discount = bulk_discount > 0 or company_subsidy > 0
|
|
use_google_auth = bool(google_client_id)
|
|
auth_mode = "google" if use_google_auth else "local"
|
|
|
|
form_config = {
|
|
"authMode": auth_mode,
|
|
"meals": menu["meals"],
|
|
"roster": config.get("roster", []),
|
|
"week": week,
|
|
"submitUrl": f"{api_url}/api/submit-order" if api_url else "/api/submit-order",
|
|
"statusUrl": (
|
|
f"{api_url}/api/form-status/{week}"
|
|
if api_url
|
|
else f"/api/form-status/{week}"
|
|
),
|
|
"rosterUrl": f"{api_url}/api/roster" if api_url else "/api/roster",
|
|
"adminUrl": f"{api_url}/api/admin/orders" if api_url else "/api/admin/orders",
|
|
"adminPdfUrl": (
|
|
f"{api_url}/api/admin/summary-pdf" if api_url else "/api/admin/summary-pdf"
|
|
),
|
|
"bulkDiscount": bulk_discount,
|
|
"companySubsidy": company_subsidy,
|
|
"googleClientId": google_client_id if use_google_auth else "",
|
|
"deadline": deadline,
|
|
"scrapedAt": scraped_at,
|
|
"hasDiscount": has_discount,
|
|
}
|
|
|
|
env = Environment(
|
|
loader=FileSystemLoader(str(TEMPLATES_DIR)),
|
|
autoescape=select_autoescape(["html", "xml", "j2"]),
|
|
)
|
|
template = env.get_template("form.html.j2")
|
|
|
|
return template.render(
|
|
config=form_config,
|
|
config_json=_json_for_script(form_config),
|
|
css=(TEMPLATES_DIR / "form.css").read_text(encoding="utf-8"),
|
|
form_js=(TEMPLATES_DIR / "form.js").read_text(encoding="utf-8"),
|
|
admin_js=(TEMPLATES_DIR / "admin.js").read_text(encoding="utf-8"),
|
|
week=week,
|
|
scraped_at=scraped_at,
|
|
deadline=deadline,
|
|
has_discount=has_discount,
|
|
bulk_discount=bulk_discount,
|
|
company_subsidy=company_subsidy,
|
|
bulk_discount_label=f"{bulk_discount:g}",
|
|
company_subsidy_label=f"{company_subsidy:g}",
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Generate meal order form HTML")
|
|
parser.add_argument(
|
|
"--api-url", default="", help="API Gateway base URL (cloud mode)"
|
|
)
|
|
parser.add_argument(
|
|
"--bulk-discount",
|
|
type=float,
|
|
default=None,
|
|
help="Bulk discount percentage (e.g., 10 for 10%% off)",
|
|
)
|
|
parser.add_argument(
|
|
"--company-subsidy",
|
|
type=float,
|
|
default=None,
|
|
help="Company subsidy percentage (e.g., 50 for 50%% off after bulk discount)",
|
|
)
|
|
parser.add_argument(
|
|
"--google-client-id",
|
|
default=None,
|
|
help="Google OAuth Client ID for Sign-In authentication",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
config = load_config()
|
|
menu = latest_menu()
|
|
|
|
bulk_discount = (
|
|
args.bulk_discount
|
|
if args.bulk_discount is not None
|
|
else config.get("bulk_discount_percent", 0)
|
|
)
|
|
company_subsidy = (
|
|
args.company_subsidy
|
|
if args.company_subsidy is not None
|
|
else config.get("company_subsidy_percent", 0)
|
|
)
|
|
google_client_id = (
|
|
args.google_client_id or config.get("google_client_id", "")
|
|
).strip()
|
|
if args.api_url and not google_client_id:
|
|
try:
|
|
import boto3
|
|
|
|
ssm = boto3.client("ssm")
|
|
resp = ssm.get_parameter(Name="/meal-order-manager/google-client-id")
|
|
google_client_id = resp["Parameter"]["Value"].strip()
|
|
except Exception as exc:
|
|
parser.error(
|
|
"Google auth is required in cloud mode, but the Google client ID "
|
|
f"could not be loaded from SSM: {exc}"
|
|
)
|
|
if args.api_url and not google_client_id:
|
|
parser.error("Google auth is required in cloud mode")
|
|
|
|
html = generate_form(
|
|
menu,
|
|
config,
|
|
api_url=args.api_url,
|
|
bulk_discount=bulk_discount,
|
|
company_subsidy=company_subsidy,
|
|
google_client_id=google_client_id,
|
|
)
|
|
|
|
week = datetime.now().strftime("%Y-W%U")
|
|
output_file = OUTPUT_DIR / f"order-form-{week}.html"
|
|
with open(output_file, "w") as f:
|
|
f.write(html)
|
|
|
|
mode = "cloud" if args.api_url else "local"
|
|
print(f"Generated order form ({mode} mode): {output_file}")
|
|
print(f" {menu['meal_count']} meals from menu scraped {menu['scraped_at'][:10]}")
|
|
if mode == "local":
|
|
print(" Start the server with: python3 src/server/app.py")
|
|
else:
|
|
print(f" API endpoint: {args.api_url}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|