Wagtail integration - #707
Closed
nofurtherinformation wants to merge 48 commits into
Closed
Conversation
Prep for the Wagtail CMS service sharing this database: - districtrmaps_to_groups and districtrmap_overlays get a surrogate integer id PK (composite PKs block row editing in Django admin forms); the original column pairs remain as UNIQUE constraints. - alembic env.py include_object now ignores the `admin` schema and django_/auth_/wagtail/taggit_/token_blacklist_ table prefixes so autogenerate never proposes dropping CMS-owned tables. Also removes a stray debug print. - Declare idx_districtrmap_overlays_overlay_id on the model so autogenerate stops proposing to drop it (pre-existing drift). Verified: alembic upgrade on local PostGIS db; autogenerate produces no junction-table or django-table ops (tested with decoy django_*/ admin-schema tables); full backend suite 256 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New top-level cms/ Django 5.2 + Wagtail 7.0 LTS project, deployed as a third Fly app (districtr-v2-cms[-dev]) sharing the existing Postgres: - All Django/Wagtail tables live in a dedicated `admin` schema via search_path (bootstrap_schema management command creates it before migrate; runs in the Fly release command and compose entrypoint). Verified: 63 tables in admin, zero in public, and backend alembic autogenerate stays clean with the Wagtail tables present. - Locales configured for en/es/zh/vi/ht/pt (wagtail-localize). - /health endpoint with DB check for Fly http checks and compose. - Prod image: gunicorn + whitenoise (collectstatic verified in build); dev: runserver via docker-compose `cms` service on :8001. - CI: test-cms.yml (postgis service, checks, migrate, missing-migration gate, tests) and fly-deploy-cms.yml mirroring the api workflows. Part of the Wagtail cutover (plan: CMS + user management replacing Auth0, data admin over existing tables, content migration). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cms/authapi replaces Auth0 as the token issuer: - KidTokenBackend adds the RFC 7638 thumbprint of the verifying key as the JWT kid header (PyJWKClient in the FastAPI backend selects keys by kid; SimpleJWT emits none by default). - /.well-known/jwks.json serves the active verifying key (+ optional JWT_NEXT_VERIFYING_KEY during rotation). - Token claims: sub, email, name, roles, and a space-delimited scope claim derived from group membership (admin/editor/reviewer/partner, created by data migration) mirroring backend TokenScope verbatim. - /api/token/ (throttled 10/min) + /api/token/refresh/ with rotation and blacklist; 10-min access, 14-day refresh. - Management commands: generate_jwt_keys, provision_users (CSV + password-setup email; Wagtail has no invite flow), issue_service_token (replaces the Auth0 client-credentials path). - Contract tests replicate the FastAPI verifier exactly (kid-based JWKS key selection, audience/issuer, scope claims): 11 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Settings: AUTH0_DOMAIN/AUTH0_API_AUDIENCE/AUTH0_ISSUER/AUTH0_ALGORITHMS become AUTH_JWKS_URL/AUTH_AUDIENCE/AUTH_ISSUER/AUTH_ALGORITHMS — the verifier is issuer-agnostic (any JWKS + RS256 + scope claim). - Remove the Auth0 gty=client-credentials scope bypass: service tokens now carry explicit scopes (cms issue_service_token) and are checked like any other token. - Cross-service contract test (tests/test_auth_contract.py) mints tokens exactly as cms/authapi does — same claim layout, RFC 7638 kid, PyJWKClient kid-matching kept in the loop — and asserts scope enforcement, expiry, audience/issuer, unknown-kid and bad-signature rejection. Mirror lives in cms/authapi/tests.py. - Env files and test-backend.yml updated to the new variable names. cms/, comments/, thumbnails/ endpoints unchanged — scopes flow through SecurityScopes exactly as before. Full backend suite: 266 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- @auth0/nextjs-auth0 removed; next-auth v5 (beta.31) added. - src/auth.ts: Credentials provider POSTs to the CMS /api/token/; jwt callback silently refreshes via /api/token/refresh/ 60s before expiry, persisting BOTH rotated tokens; refresh failure sets an error flag that forces re-login. - New /auth/login credentials page (Radix UI, sanitized returnTo) and /auth/logout route preserve the existing URL contract (useAuthRoutes); NextAuth endpoints live under the same /auth basePath. - lib/auth0.ts replaced by lib/auth.ts exporting the same ClientSession shape — factory.ts, cms.ts, generateThumbnail.ts, Providers, cmsFormStore needed only import-line changes; PermissionGuard (scope-claim parsing) untouched. - proxy.ts gates /admin on the NextAuth session; roles come from the token's roles claim (AuthButton, admin layout updated). - Env: CMS_URL / NEXT_PUBLIC_CMS_URL / AUTH_SECRET replace AUTH0_* in .env.docker.example. Verified: bun run build passes with zero type errors; no @auth0 imports remain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AUTH_SECRET is a Fly runtime secret; the deploy workflow now injects NEXT_PUBLIC_CMS_URL/CMS_URL per branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cms/datastore mirrors six Alembic-owned public tables with managed=False models (Django never issues DDL; Alembic stays sole schema owner): districtrmap, gerrydbtable, map_group, overlay, and the two junction tables (using their new surrogate id PKs). - Wagtail "Data" menu (SnippetViewSetGroup): DistrictrMap with filters/search and panelled edit form (uuid read-only), full CRUD for overlays/groups/junctions, read-only GerryDB tables via a deny-all-writes permission policy. - Postgres enums mirrored as TextChoices verbatim from backend/app/models.py (incl. GeoUnitType's "bg" value); JSONB → JSONField, arrays → ArrayField; all FKs DO_NOTHING + db_constraint False. Partitioned/document-schema tables deliberately unmapped. - check_mirror_drift command compares mirrors against information_schema (names + nullability, per-table excluded-columns allowlist); wired into test-cms.yml after running the backend's alembic migrations, with backend schema paths added to the workflow triggers. - Permissions: admin group granted all datastore perms via data migration; other groups none by default. Verified: 26 cms tests green, drift check passes against the live dev db, makemigrations --check clean, authenticated smoke test of all six admin views. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /api/admin/gerrydb/import (scope create:districtr_maps) lets the CMS admin trigger GeoPackage imports over HTTP, replacing the engineer-only CLI flow. Reuses management.load_data.import_gerrydb_view verbatim via BackgroundTasks; the task owns its session (request sessions close at teardown). Early 422 validation restricts layer and table names to SQL-identifier-safe characters and requires a .gpkg path. Also fixes a latent circular import: management/load_data.py imported get_session from app.main (it lives in app.core.db) — mounting the new router surfaced the cycle through cli.py. 19 new tests (auth required, scheduling args, identifier validation, session ownership, failure logging); full suite 285 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cms/content replaces the legacy FastAPI cms module's authoring model:
- StreamField blocks mirror the TipTap custom nodes verbatim
(boilerplateNode, sectionHeaderNode, planGalleryNode, formNode,
mapCreateButtonsNode, commentGalleryNode) with the exact camelCase
attribute names from app/src/app/constants/cms.ts, so the existing
React components consume block values as props unchanged. Map-slug
attrs are lazy ChoiceBlocks over the datastore mirrors.
- TagPage/PlacePage under per-locale TagsIndexPage/PlacesIndexPage;
lookup = type + slug + locale, matching the legacy unique key.
PlacePage keeps districtr_map_slugs as ArrayField (round-trips the
legacy varchar[]). wagtail-localize translations share
translation_key; Wagtail live/draft replaces published/draft columns.
- Public compat API: GET /api/content/{tags|places}/slug/{slug}
(live-only, en fallback, available_languages, CORS) and /list,
mirroring the legacy response semantics.
- migrate_tiptap command: reads cms.tags_content/places_content via
raw SQL, converts ProseMirror docs (custom nodes -> blocks; prose
runs -> rich_text HTML), en page canonical + localized translations,
published -> live revision + draft -> unpublished revision,
idempotent by (type, slug, locale). --dry-run emits per-row node/
block counts and a normalized text diff; any text loss fails the run.
Verified: 61 cms tests green; live 4-row fixture migration with
dry-run gate, compat API curls (en/es/fallback/404), and idempotent
re-run (0 created, 4 unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- utils/api/cmsContent.ts: typed client for the CMS content API (server-side CMS_URL for SSR-in-docker, NEXT_PUBLIC_CMS_URL in the browser); body typed as a discriminated union over the StreamField block types, gallery/button values typed as the existing component prop types. - StreamRenderer maps blocks to the SAME components the TipTap renderer used: rich_text/boilerplate HTML through the existing html-react-parser pipeline + domNodeReplacers, section_header -> ContentHeader (keeps HeaderSecondTierNav h2 anchor scan working), plan_gallery/comment_gallery/form/map_create_buttons -> existing React components with camelCase props passed through. - tag/place detail + tags/places index pages switched to the new client; LanguagePicker fed by available_languages; en fallback delegated to the CMS; URL structure and revalidate unchanged. - Legacy cms.ts/RichTextRenderer/TipTap editor left untouched for the decommission workstream. Known follow-up: PlaceMap/utils.tsx still uses the legacy list client (needs per-place districtr_map_slugs, not exposed by the new list endpoint yet). Verified: bun run build zero type errors; rendered HTML greps of /tag/fair-maps-co (en + es cookie), /place/massachusetts, index pages, and draft-only 404 against the live dev stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wagtail admin gains two pages inside the Data menu (admin group only): - Import GeoPackage: upload a .gpkg (or point at an existing s3:// path) + layer/table name; streams to gerrydb-uploads/ in the bucket (R2-vs-S3 conditional mirrors the backend's env contract) and schedules the import via POST /api/admin/gerrydb/import using an in-process 15-minute service token (scope create:districtr_maps). - Thumbnails: trigger map/document thumbnail regeneration via the existing backend endpoints (scope create:content). Also fixes two latent provision_users bugs that would have broken cutover-day account setup emails: PasswordResetForm silently skips unusable-password users (exactly what provisioning creates) and crashes without a request when contrib.sites is absent — now uses an AccountSetupForm subclass and derives the link domain from WAGTAILADMIN_BASE_URL. Branded password-setup email templates added. BACKEND_API_URL added to settings/env/fly.toml. 97 cms tests green; end-to-end mint->POST->verify wiring proven against the live compose backend (expected 403 with an ephemeral dev key). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New cms/galleries app replicating the power-user fork's gallery
sections (consultant drafts, public gallery, works in progress, COI
gallery):
- Gallery snippet (Workflow+DraftState+Revision mixins, Clusterable)
with section, visibility (public|group_only), optional MapGroup FK
scoping hook, rich-text description, and orderable entries
referencing Document public_ids.
- Own "Galleries" admin menu (Data group is admin-only and would never
render for partners). Permission split via data migration: partners
add/change (draft-only — no publish_gallery perm), editors/admins
publish; wagtailadmin.access_admin granted to all four groups (was
granted nowhere; provision_users expected it).
- Public API: GET /api/galleries/{slug} (live only; group_only
requires a valid Districtr-issued Bearer token — role/group matching
left as the documented next step) and a list endpoint with
section filtering + entry counts. CORS like the content API.
- Next.js /gallery/[slug] page renders title/description and the
existing PlanGallery component with entry public_ids.
Verified: 115 cms tests green (18 new); live API curls (200/403/404,
CORS, token from /api/token/); bun build clean; SSR page renders the
demo gallery and 404s unknown slugs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Partial state from the decommission workstream, interrupted by a usage limit. Done so far: backend/app/cms/ deleted, router import removed from main.py, env.py drops the model imports and now skips the cms schema in autogenerate (legacy cms.tags_content/places_content must survive for migrate_tiptap), test_cms.py deleted. NOT yet done/verified: backend suite run after deletion, autogenerate check, cms list-endpoint extension (districtr_map_slugs), app-side removal (admin/cms pages, TipTap editor machinery, legacy cms.ts client + PlaceMap/utils.tsx switch, admin/config.ts Wagtail link, unused @tiptap deps), bun build, lint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the decommission started in the WIP commit (backend cms module deletion — verified: 266 passed, exactly the 19 deleted cms tests fewer; autogenerate ignores the cms schema so the legacy tags_content/places_content tables survive for migrate_tiptap). App side: - /admin/cms pages, ContentEditor, ContentList, ContentPreviewModal, RichTextEditor + TipTap node/nodeview machinery, RichTextRenderer (TipTap-JSON path), useCmsEditorConfig, and the legacy utils/api/cms client are deleted. Kept (used by StreamRenderer and static pages): PlanGallery, CommentGallery, MapCreateButtons, BoilerplateContent/ NodeRenderer (now HTML-string-only, no @tiptap), HeaderSecondTierNav, DomNodeRenderers. - cmsFormStore trimmed to the session slice (name kept to avoid churn in review/thumbnails/PermissionGuard/Providers/AuthButton). - PlaceMap homepage store now uses the CMS list endpoint, which gains districtr_map_slug(s) fields (cms/content/api.py + tests). - Admin landing CMS card links to the Wagtail admin (NEXT_PUBLIC_CMS_URL/admin/). - All 9 tiptap packages removed from package.json. Verified: cms suite 116 green; bun run build zero type errors (after clearing stale .next route types); homepage 200; pre-commit Passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /api/admin/districtr-map/compose (scope create:districtr_maps) builds a complete DistrictrMap from layers already registered in gerrydbtable, replacing the multi-step CLI flow: shatterable view (when a child layer is given, named <slug>_shatterable), create map (hidden by default), extent, parent-child edges, optional group assignment — chained in a background task that owns its session, mirroring the gerrydb import endpoint. In-request validation: slug/layer regexes (422), unknown layer/group (404), duplicate slug (409) — nothing schedules on failure. +28 tests asserting status codes, exact step order/kwargs for both shatterable and single-layer variants, and logging. Full backend suite: 294 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two new Data-menu pages in the Wagtail admin: - Upload overlay (perm datastore.add_overlay): .geojson/.pmtiles file (or existing s3/http path) -> S3 overlays/ prefix -> public URL via OVERLAY_PUBLIC_URL_BASE (tilesets CDN in prod) -> Overlay row + selected map attachments in one transaction. pmtiles requires source_layer; custom_style JSON validated. - Compose map module (perm datastore.add_districtrmap): existing gerrydb layers as parent/child dropdowns + slug/districts/tiles/ group; calls the new backend compose endpoint with a service token; modules are created hidden until reviewed. Plus three external menu links (Comment review, District comments, Thumbnails) pointing at the Next.js admin via FRONTEND_URL, shown to reviewer/editor/admin — one pane of glass for all review processes. 46 new tests; full cms suite 162 green; both pages and menu items smoke-tested live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewers can now be limited to specific comment tags, managed in the Wagtail admin and enforced by the backend: - cms: ReviewTagAssignment (user + tag_slug, admin-managed "Review tag scopes" snippet). Tokens mint a sorted review_tags claim when the user has assignments (absent = unrestricted, back-compat for internal reviewers); scopes_for_user strips read:read-all for assigned non-admin users so the claim actually bites; rides the refresh token through silent refresh. - backend: allowed_review_tags helper (read:read-all or absent claim = unrestricted). /admin/list applies an at-least-one-allowed-tag EXISTS filter (the existing tags param uses AND semantics and would have over-restricted multi-tag reviewers; untagged comments are invisible to restricted reviewers) and intersects requested tags. /admin/review 403s on out-of-scope tags/comments and on commenters (not tag-scoped). District comments are tag-less: restricted reviewers get an explicit 403. Tests: +6 cms (claim minting, scope stripping, refresh propagation), +12 backend (list filtering, intersections, read-all override, review 403s, district-comments policy), contract tests carry the new claim. cms suite 168 green; backend 306 passed; lint clean. Note: 3 pre-existing order-dependent TestCommenterEndpoint failures reproduce on a targeted subset run without these changes; they pass in the full suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the 10 verified findings from the branch review, most-severe first: - Refresh-token races (forced logout every 10 min): CMS keeps rotation but drops BLACKLIST_AFTER_ROTATION (RSCs cannot persist rotated cookies, so single-use refresh tokens deterministically brick sessions); frontend adds a single-flight refresh guard and narrows the middleware matcher to /admin only. - Editors locked out of all content pages: data migration grants GroupPagePermission rows (add/change/publish on root) to editor and admin; own-content-only editing recorded as a deferred product decision. - Empty published gallery listed EVERY document (drafts included): empty-state render + getPlans empty-array guard. - Rich text authored in Wagtail served unexpanded <a linktype>/<embed>: FrontendRichTextBlock applies expand_db_html in the API representation (rich_text + boilerplate.customContent); dead 'underline' feature removed. - Admin store froze a 10-minute token: new /auth/token route (runs the refresh in a cookie-writable context) + 4-minute session polling in Providers. - group_only galleries 404'd for everyone: gallery pages are dynamic now and forward the session bearer with no-store (never cached shared). - Stale /admin/cms redirect and footer link -> /admin. - Non-English-only pages vanished from indexes: listCMSContent no longer forces language=en; list endpoint contract pinned by test. - Wagtail review links now match each page's scopes (editor loses comment-review links, reviewer loses thumbnails; tag-scoped reviewers don't see District comments). - Alembic include_object: prefix regex removed in favor of the schema contract (it would have silently excluded future public tables named content_*/auth_*/etc.). Plus verified cleanups: review-tag enforcement now flows through a review_auth dependency (unforgettable for future endpoints); admin_ops session-branch duplication collapsed via nullcontext; PermissionGuard uses a shared base64url-safe decodeJwtPayload; dead scope plumbing, duplicate sanitizeReturnTo, unused listGalleries, and the store's unused subscribeWithSelector removed. Verified: backend 306 passed; cms 175 tests green (+7); frontend build zero type errors with runtime smoke (gallery/tag/admin redirects); alembic autogenerate clean against live Wagtail tables; lint passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _post_backend collapses the four backend-POST functions; all of them now surface the backend's JSON detail in errors (previously only compose did). - _upload collapses the gpkg/overlay upload duplication. - mint_service_token moves to its canonical home in authapi.tokens; the management command and datastore services delegate to it. - Shared cms/core/api.py provides the CORS/JSON response helpers used by both the content and galleries public APIs. 175 cms tests green; no behavior changes beyond improved error detail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hand-off notes for future sessions: deferred product decisions (editor own-content-only, group_only gallery enforcement, refresh blacklist trade-off), small functional follow-ups, measured perf items, infra consistency work, and the operational cutover checklist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rge head - place/portal generateMetadata used the legacy getCMSContent(slug, lang, type) arg order and published_content shape; align with the Wagtail client - TestReviewTagScoping still sent recaptcha_token (renamed to turnstile_token on dev) - test_config monkeypatched ACCOUNT_ID, which no longer exists in Settings - alembic merge revision for the dev + wagtail-cutover heads Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ening Review findings: scoping was enforced by enumerating dangerous views, and Wagtail has more object-level endpoints than were enumerated — anything not on the list defaulted to open. - Pages: register before_unpublish/copy/move_page and before_bulk_action (bulk delete/publish/unpublish fire ONLY the bulk hook) - Galleries: scoped Copy view (stock CopyView prefills via bare get_object_or_404 with an unrestricted map_group chooser) and the generic before_unpublish hook (UnpublishView checks only model-level publish) - Datastore: History/Usage views get the same 404 guard as Inspect - PlacePageForm no longer silently drops other teams' maps (or their curated order) from shared place pages - content/galleries APIs: negative limit no longer 500s (shared clamped pagination in core/api.py); available_languages deduped - Trim: TeamScopedViewSetMixin + instance_in_scope collapse the per-app scoping copies; 47-line Wagtail base.html copy replaced with a recursive same-name extends; dead Team.slug field removed; stale e2e endpoint removed - Regression tests for each closed path (212 cms tests green) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, StaticPage Product decisions (Dylan, 2026-08-04): - Editors edit only their own content: content/0004 revokes the editor group's tree-wide change_page; add_page + Wagtail's owner model grant edit on owned pages, publish_page applies only to those. migrate_tiptap gains --owners "auth0|sub=email,..." to set Page.owner from the legacy author column (Auth0 subjects) so pre-cutover content stays editable. - group_only galleries enforced via Teams: JWT gains a map_groups claim (user's teams' MapGroup slugs); the gallery API requires the gallery's map_group in that claim or the admin role — any-valid-login no longer opens restricted galleries. - StaticPage type (+ StaticIndexPage) with /api/content/static/... served by a Next.js /[slug] catch-all; hardcoded routes take precedence, so static pages migrate into the CMS one at a time. - District-comments 403 for tag-scoped reviewers: decided to keep as-is. FOLLOWUPS doc updated; cms 220 tests, backend suite, typecheck + build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
listCMSContent pages through the server's 100-row cap so /places, /portals, and the homepage PlaceMap don't silently truncate once slug x language rows exceed one page. Remaining files: ruff/prettier auto-fixes from pre-commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Narration and duplicated context only — every contract comment (schema split, JWT blacklist trade-off, alembic exclusion, block naming) stays: - settings DATABASES / teams docstring / config s3_bucket: deduplicate against their module docs - datastore hooks: drop FK-widget inline comments the module docstring covers - content forms + hooks docstrings tightened - Providers: drop a line narrating the guard clause Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m infra Fly is being deprecated; the CMS now deploys to ECS like everything else: - infra/cms.ts: one small Fargate task (512/1024, no autoscaling — ~20 admins, ISR-shielded public reads) + cms-migrate release task (bootstrap_schema + migrate), task-role S3 access to the GPKG bucket - ALB: cms target group (health /healthz), host rule for cmsDomain, cert SAN, DNS record in the dnsRecords output; cms SG + RDS ingress from it - backend/frontend task envs swapped from Auth0 to the CMS issuer (AUTH_JWKS_URL/AUTH_ISSUER/AUTH_AUDIENCE; frontend: AUTH_SECRET + CMS_URL); auth0* config keys removed from config.ts and both stack files - cms: /healthz middleware answers ALB probes before host validation (static 200, no DB — mirrors the backend TG decision); get_s3_client falls back to the task role via AWS_USE_DEFAULT_CREDENTIALS - .github/workflows/deploy-cms.yml mirrors deploy-api.yml - FOLLOWUPS checklist step 2 rewritten AWS-first Pending human steps: pulumi config set --secret djangoSecretKey, jwtSigningKey, jwtVerifyingKey, authSecret, resendApiKey (per stack) and the cms.* DNS records. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New cms/moderation app: comment review, district-comment review, and the under-construction toggle are thin Wagtail views over the existing FastAPI admin endpoints, authenticated with a short-lived per-user JWT (authapi.serializers.mint_user_access_token) so scope and review_tags enforcement stays in the backend. Replaces the external "Comment review" menu link (FRONTEND_URL is gone from settings/env/infra) and the entire app/src/app/admin tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
authapi.0007 retires editor and reviewer (members move to partner): partner now covers the whole partner-org role — own-content page editing (root add_page, owner-model edits, no publish: that goes through admin review), gallery drafting, and comment moderation (scopes.py grants create:content_review). New super_partner adds the datastore map-module permissions; GPKG import splits onto its own add_gerrydbtable gate and stays admin-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Map submissions (feature list: partners manage map submissions): the backend admin comments list gains a has_document filter, and a new Wagtail queue lists plan-carrying comments with approve/reject plus one-click add-to-gallery — the plan lands as a draft revision (entries live in revision content) scoped to the user's team galleries. Review//approve: content.0007 provisions the "Admin approval" Workflow (GroupApprovalTask for admin) on the page tree and Gallery snippets, so partner drafts ship via Submit for moderation. content.0006 pre-provisions the six content-language Locale rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
545e708aeb30 accidentally dropped the ON DELETE CASCADE that dc0216fef023
put on the districtrmaps_to_groups FKs (its own downgrade restores it), so
deleting a DistrictrMap or MapGroup died with an IntegrityError from the
link table; c8f3a1d92e47 restores both cascades. e5b7c2f81a93 does the same
for parentchildedges (derived per-map rows; the empty partition left behind
is cosmetic). The document FK keeps NO ACTION deliberately — maps with
saved plans must not be deletable — and the Wagtail delete/bulk-delete
paths now refuse those with a message instead of a 500.
Also: BACKEND_API_URL in cms/.env.docker (moderation views were calling the
cms container itself, hence Django 404s on /api/comments/admin/list) and
fix the multi-line {# #} template comment in the admin base override that
rendered as literal header text (Django comments are single-line).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The org/access boundary moves off MapGroup (districtr_v2-i06): Team gains a slug (authapi.0008) minted as the JWT `teams` claim; galleries get a required, real `team` FK (galleries.0003 — structurally closing the ownerless group_only gallery hole, districtr_v2-rqz); and teams own map modules directly through TeamDistrictrMap, with existing TeamMapGroup ownership expanded one row per group member map at migration. The scoping engine (authapi/teams.py) now keys on team ids/slugs; galleries, pages, map modules, and the moderation add-to-gallery flow follow through their per-resource lookups. MapGroup and its junction stay as the product listing facet only — no access control reads them anymore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arity
migrate_tiptap had a critical silent-loss bug: legacy rows wrap the
ProseMirror doc in {title, subtitle, body}, the converter expected a bare
doc, and every page migrated EMPTY while the fidelity check compared "" to
"". The converter now unwraps (or hard-fails on unknown shapes, naming the
row), maps wrapper title/subtitle onto the page, and the real run lands
54 created / 1 updated with zero text loss and clean idempotent reruns.
Preview and View-live no longer 500 (districtr_v2-nti): content pages set
preview_modes=[] and generate frontend URLs (FRONTEND_URL restored to
settings) — Wagtail never serves these pages. Index pages are provisioned
by migration (content/0008, shared provision.py) and locked with
parent_page_types/max_count. plan_gallery gains curated-gallery wiring: a
gallerySlug choice on the block, frontend fetch of /api/galleries/<slug>,
entry ids as the plan filter. Other blocks verified at parity with the
legacy TipTap components.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n UX The DistrictrMap edit page now manages everything about a module in one place: overlay links, map-group listings, and (admin-only) team assignments as inline formsets saved atomically with the map, plus a regenerate-thumbnail button. The two link-table snippet listings leave the menu; the Thumbnails tool is document-only. New role-aware dashboard panel (core/wagtail_hooks.py) surfaces each group's next actions; Reports hidden for non-admins; Districtr palette via insert_global_admin_css. Moderation: "Flagged comments" menu item, tag-scoped reviewers no longer see the buttons/links the backend 403s by design, and deleting a Team that owns galleries gets a friendly refusal instead of a PROTECT 500. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t import
Menus now say what you do, not what tables exist: "Map modules" (Create map
module first, then Edit map modules / Edit overlays / Map groups / Upload
overlay / Plan thumbnails), one "Review" submenu (comments, flagged queue,
map submissions), and a "Site content" submenu with direct entry points to
edit portal, place, and static pages (place pages had no discoverable path).
Create map module is one page end-to-end: overlay selection rides the
compose request (backend gains overlay_ids on /districtr-map/compose and
attaches the links in the compose task), slug collisions are caught in the
form instead of a backend 4xx, and a team-scoped composer's new module is
auto-assigned to their teams (short poll for the async row; falls back to a
message when composition is slow).
Removed for now: GPKG import UI (raw data uploads deferred; service plumbing
and the backend endpoint stay), the GerryDB tables listing, and district
comment review (portal comment review remains the moderation surface).
content/0010 wraps migrate_tiptap as a REVERSIBLE data migration: forward
no-ops without the legacy tables and is idempotent; reverse deletes exactly
the pages matching legacy (slug, language) rows. Round-tripped against the
dev DB: 55 deleted on reverse, 55 restored on re-apply, hand-made pages
untouched. Also fixes the admin-header comment leak (nested {# #}).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…te buttons TagPage (portal) map selection is a proper dropdown for every role — admins included, who previously typed slugs freehand — with team scoping narrowing the choices as before and saved-but-missing modules kept selectable. PlacePage gains an orderable multi-select widget (add dropdown + move up/down/remove, posted as ordered hidden inputs): the saved order is the display order on the public place page, and a scoped member's reordering now actually applies, with other teams' modules pinned to their original positions. MapCreateButtons blocks render the same MapStartCard grid used on place pages (megaphone keeps its banner, cards inside), and portal pages get vertical padding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review is now one flow matching how partners think: pick a portal you have access to (team-scoped via the portal's map module, further narrowed by ReviewTagAssignment — a portal's page slug is its comment tag), then review its submissions with a Comments / Map submissions toggle. The portal supplies the tag filter; flagged/status/commenter filters and the add-to-gallery action carry over. The standalone comment, flagged, and map-submission queues and their menu items are gone. Menu trims: Plan thumbnails tool and Map groups listing removed (map thumbnails still regenerate from the module edit page; map_group remains a backend listing facet); Wagtail's Reports relabelled "Admin analytics" for admins (still hidden for everyone else). Wagtail's stock "Welcome to your new Wagtail site!" home page is renamed "Districtr" (content/0011, reversible) — the level itself is structural. Static catch-all pages get the same vertical padding as portals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review tag scopes are gone as a concept: the JWT review_tags claim is now derived from the user's teams' portals (a portal's page slug is its comment tag slug) — creating a portal IS the assignment. Admins carry no claim; team-scoped users get their portal slugs; team-less non-admins get an empty claim, which the backend treats as "allows nothing" — moderation now fails CLOSED until a partner joins a team. Partner scopes drop read:read-all (the backend's unrestricted-read escape hatch that would have neutered the claim). ReviewTagAssignment, its snippet UI, and the two-mechanism split are deleted; the content API injects the portal slug into comment-form blocks' mandatory tags so tagging never depends on author discipline. Galleries are no longer a separate abstraction: the plan_gallery block's ordered `ids` list IS the curated gallery, living on the portal/static page itself — page team-scoping owns access, the page workflow owns approval, and the frontend reads entries straight from the page body (no second fetch; group_only visibility is retired with the model). The review queue's "Add to portal gallery" appends to the portal page's own gallery block as a draft revision. The galleries app survives as migration history only (authapi/0007 and content/0007 depend on it); galleries/0004 drops the tables after content/0012 folds existing gallerySlug references into inline ids (dev's demo gallery converted cleanly). The teams JWT claim, /api/galleries/*, and the /gallery/[slug] route go with it. Dashboard shortcuts are now action cards whose labels match the sidebar actions exactly; the raw Pages tree is hidden for non-admins (Site content covers it); the shortcuts panel gets padding and loses another multi-line template comment rendering as text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…text Overlay dropdowns everywhere now read "Name — line · pmtiles (file)" so the line/text/fill overlays sharing a data source are tellable apart, and sort by (name, layer type) so a source's pair sits adjacent. Upload overlay takes multiple layer types in one submission — the frequent line+text case creates both overlays from the same source, type-suffixed, each attached to the selected maps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First-pass consolidation before anything ships: 27 migrations across four apps collapse to 6 that express only the end state — three regenerated initials (authapi, content, datastore), authapi/0002_provision_roles (the three groups and every grant), content/0002_provision_site (locales, index pages, home rename, admin-approval workflow), and content/0003_import_legacy_content (the reversible tiptap wrapper). The galleries app is deleted outright — nothing needs its migration history once the graph is rebuilt — along with the gallery_slug_choices stub kept only for a serialized migration reference. The intermediate data migrations (editor/reviewer consolidation, TeamMapGroup conversion, gallerySlug folding) exist nowhere fresh installs can need them; the dev database was rebooked with --fake and its stale galleries content-type rows purged. The full test suite rebuilds the fresh chain every run. DRY: GroupMenuItem moves to core/menu.py (moderation + content share it); the per-module test factory copies (PASSWORD, make_user, make_admin_user, make_team, make_portal, create_mirror_tables) consolidate into core/testing.py; migration-number prose updated to the squashed names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…licy 251 -> 235 tests: gone are the assertions that mirror configuration back at itself — menu label/order/url lists, exact dashboard card enumerations per role (replaced by one gating test: partner ⊂ super_partner ⊂ admin), per-view repeats of the same permission gate that ToolAccessTests already proves once, Django-validator tautologies, historical removed-URL checks, and redundant tag-scoped UI variants. Added one unit test for the shared GroupMenuItem gating primitive. What stays untouched is the half of the suite that is genuinely mission-critical: the cms<->backend JWT contract, team-scoping enforcement, converter/migration data fidelity, cross-service payload shapes, delete guards, and product-decision encodings. The policy is now written down in AGENTS.md + CLAUDE.md: test boundaries and decisions, never configuration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Agentic rough out of CMS/DMS.