diff --git a/backendServer/backend/settings/prod.py b/backendServer/backend/settings/prod.py index 2f7de60..113b167 100644 --- a/backendServer/backend/settings/prod.py +++ b/backendServer/backend/settings/prod.py @@ -11,6 +11,13 @@ # sets instead (proxy_set_header X-Real-IP $remote_addr;). RATELIMIT_IP_META_KEY = "HTTP_X_REAL_IP" +# nginx terminates TLS and proxies to gunicorn over a Unix socket, so Django never +# sees an HTTPS connection directly — request.is_secure() (and anything built on it, +# like build_absolute_uri()) would compute False for every request, producing +# http:// URLs on an https:// site. Trust the header nginx always sets instead +# (proxy_set_header X-Forwarded-Proto $scheme; on every server block). +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # crash at startup if missing ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",") diff --git a/backendServer/broadcast/adapters/_helpers.py b/backendServer/broadcast/adapters/_helpers.py index e14195d..7fc562c 100644 --- a/backendServer/broadcast/adapters/_helpers.py +++ b/backendServer/broadcast/adapters/_helpers.py @@ -110,6 +110,41 @@ def full_address(ev) -> str: return f"{ev.address_line1}, {mid}{ev.state} {ev.zip}" +_FREE_TEXT_RE = re.compile(r"\bfree\b", re.I) +_NUMBER_RE = re.compile(r"\d+(?:\.\d+)?") + + +def format_cost(is_free: bool, price: str) -> str: + """Normalize a free-text cost into the plain number destination forms expect. + + Destination sites (Triangle Weekender, ABC11) want a bare number in their + Cost field, not the free-text the submitter typed on The Commons — a range + like "5-10" gets rejected or mis-rendered. This normalizes at the adapter + boundary only; the raw string on CanonicalEvent is never touched. + + Rules (decided by the ticket owner, 46.5): + - `is_free` flag, or the word "free" anywhere in the text -> "0" + - a range ("5-10", "$5 - $10", "5–10" en dash) -> the low value, e.g. "5" + - a single value ("$12", "12") -> the bare number, "12" + - empty input -> "" (nothing to normalize; caller's required/optional + handling decides what happens next) + - non-numeric junk with no digits at all (e.g. "TBD", "call for pricing") + -> passed through unchanged. We never invent a number, and forwarding + the original text is safer than silently dropping it. + """ + if is_free: + return "0" + text = (price or "").strip() + if not text: + return "" + if _FREE_TEXT_RE.search(text): + return "0" + match = _NUMBER_RE.search(text) + if not match: + return text + return match.group(0) + + def apply_specs(page, specs, ev, timeout_ms) -> list[str]: """Fill the Playwright-fillable specs; return descriptors of missing required. diff --git a/backendServer/broadcast/adapters/abc11_community.py b/backendServer/broadcast/adapters/abc11_community.py index e55e7a2..7c1b951 100644 --- a/backendServer/broadcast/adapters/abc11_community.py +++ b/backendServer/broadcast/adapters/abc11_community.py @@ -11,6 +11,15 @@ Field ids (cfN) and the submit button (value="save") are verified against the captured dump. Category (react-select) and image (custom uploader) are best-effort; everything else maps from canonical fields. + +Image: the form has exactly one `input[type=file][accept="image/*"]`, behind a +drag-and-drop skin at `.image-field-esf .image-dropzone` — a real file input, +not a JS-only dropzone, so a programmatic file set works through the normal +Playwright/extension path. Emitted only when `ev.image_url` is set (mirrors +triangle_weekender's image spec). Uploading reveals an alt-text field on +ABC11's form; `CanonicalEvent` has no alt-text field upstream, and per the +"adapters never invent content" rule we do not synthesize one — that field is +left for manual follow-up (see docs/broadcast.md's adapter rules). """ from broadcast.adapters import _helpers as h @@ -48,24 +57,27 @@ def _duration(ev): # Map our canonical category slugs to a search term to type into ABC11's -# react-select Category box; the extension types it and picks the first option. +# react-select Category box; the extension types it and picks whichever +# rendered option is an exact (else prefix) match, per term. ABC11's +# vocabulary is a fixed 17-term list (verified live 2026-08-03 — see suite +# 46.1) with no "Music"/"Family"/etc. terms, so most of our slugs have no +# defensible match — those are left unmapped rather than guessing (a wrong +# category is worse than none; adapters never invent content). _CAT_MAP = { - "music": "Music", - "arts": "Arts", - "family-kids": "Family", - "food-drink": "Food", - "festival": "Festival", - "market": "Market", - "literary": "Literature", - "community": "Community", - "nightlife": "Nightlife", - "wellness": "Health", - "education": "Education", - "sports": "Sports", - "film": "Film", - "dance": "Dance", - "comedy": "Comedy", - "theatre": "Theater", + "music": "Theater / Concerts", + "dance": "Theater / Concerts", + "comedy": "Theater / Concerts", + "theatre": "Theater / Concerts", + "arts": "Art / Photography / Crafts", + "food-drink": "Food and Beverage / Farmer's Market", + "market": "Food and Beverage / Farmer's Market", + "festival": "Festivals / Parades", + "community": "Community Events / Volunteerism", + "education": "School / Education", + "sports": "Sports and Recreation", + # Unmapped — no defensible match in ABC11's vocabulary; left out so no + # category is sent rather than stretching to a nearest neighbour: + # family-kids, literary, nightlife, wellness, film. } @@ -80,14 +92,20 @@ def _cat_terms(ev) -> str: # Declared once; consumed by the server-side fill loop (h.apply_specs) and the # manual-review recipe export. Date/time are best-effort (optional) as before. -# NOTE: #eventStartDate-label is the field's