Import WordPress stories: full taxonomy translation, portal-matched category seeds, legacy grants - #2163
Merged
Merged
Conversation
maebeale
force-pushed
the
maebeale/story-share-importer
branch
from
August 11, 2026 07:06
a8943de to
353ffa8
Compare
maebeale
marked this pull request as ready for review
August 11, 2026 14:51
AWBW is migrating ~300 stories off the legacy WordPress site. The export is a single flat CSV; we need each row as a StoryIdea (the canonical submission record) and a connected published Story for rows that were public on WordPress, mirroring the in-app idea->story promotion flow. StoryImporter does this with no schema changes, mapping the WP fields we can represent and logging the ones we can't (original publish date, US state, free-text facilitator, source permalink/post id) so the gaps are visible rather than silently dropped. A dry-run mode reports what would be created. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Running the importer from a rake task means only engineers can do it and nobody sees what a CSV will produce until after it's written. This adds an admin-only flow from the Stories index: upload the WordPress export, see a dry-run preview (ideas/stories to be created, skipped rows, and the unmappable-field warnings) and only then confirm. The uploaded CSV is stashed as an ActiveStorage blob between preview and confirm — it's far too big for the cookie session — and purged once the import runs. Records are attributed to the importing admin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WordPress export separates multi-audience Tags with '|' (e.g. children|teens), but windows_type_for only split on commas, so every multi-audience row silently fell back to the Adult windows type (~65/309 rows in the June 2026 export). Split on both delimiters. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two side-by-side buttons in the index header read as two primary actions; import is secondary. Demote it to a subtle admin text link beside the New Story button. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The importer's guessed SECTOR_SYNONYMS covered only 14 categories and pointed several at sector names the new taxonomy doesn't use (e.g. 'Education/Schools', 'Substance Use', 'Homeless'), so ~191/309 rows went untagged. Replace it with the authoritative 'Story Share to Portal Migration' mapping, stored as an editable data file staff can maintain, and resolve the sector once per row so a dry-run preview surfaces the same tagging/warnings a real run would. Coverage rises to 274/309 rows; the 35 that remain are 'Facilitator Spotlights', which has no sector. Fixtures now use 10 real rows from the June 2026 export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The importer previously mapped only the single `Categories` value to one Sector. Real exports carry the same taxonomy across `Categories`, `User Categories`, `Tags`, and `who_is_your_story_about`, and stories need their age ranges and story populations tagged too — not just a sector. Reframe the mapping file as a unified `translations` table (each source value translates to a Sector and/or one or more typed Categories) covering every distinct value in the export, and apply all of them from all four columns. Age-twin tags apply both the AgeRange and the underscore StoryPopulation; flagged semantic overlaps (e.g. Community Engagement -> Community Building) apply alongside the direct sector. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… grants Reconcile the seeded categories to the live portal: AgeRange moves the range into the description column (clean names "Children"/"Teens"/"Adults"/"Elders"); StoryPopulation age twins take a trailing underscore so the clean names are free (reconciled first to avoid the global name-uniqueness collision); the 13 retired topic categories are unpublished (kept for history); and display positions match production. A CategoryDecorator composes "Children (0-12)" for display and strips the underscore, so the data change is invisible to users. Also seed the Cathy Salser and Babs Mayer legacy scholarship Grants so imported stories can connect through them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…holarship Stories tagged "Cathy scholarship" / "babs mayer" belong to a named legacy scholarship fund. Model that as story → author → scholarship → grant: resolve the author from the facilitator name, set it on the story, and connect the author to the Grant through a Scholarship. Warns and skips when the facilitator has no last name (a Person needs both), so no invalid records are created. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cathy Salser and Babs Mayer each fund their own legacy scholarship (a Person funder), not the AWBW organization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rovenance Reworks the importer to build richer records from each WordPress row: - Every row becomes a Story (published per the WordPress Status). A non-AWBW author's story also gets a promoted StoryIdea; AWBW-authored or author-less rows are Story-only. - Author resolved from the facilitator name (a Person); when it can't form one (single name, "AWBW") the name is kept as a Comment — so add has_many :comments to Story and StoryIdea. - Content runs through a wpautop step so the export's raw-newline paragraphs survive as HTML instead of collapsing. - Original publish Date is preserved as created_at; exact-title workshops are linked (else kept as external_workshop_title); featured flags carry over. - Organizations resolve through a new editable mapping file (variant/SKIP), and non-AWBW authors get a facilitator Affiliation; the author's profile display_name_preference syncs from the credit unless already customized. state still has no valid Address to live in (needs locality + city), so it stays a logged warning for now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e state The import preview is now row-by-row: each row's shorthand, whether its organization/author are new or reused, whether it creates a Story (and a StoryIdea), the linked/external workshop, and the sectorable/categorizable items it would tag — so staff can spot-check before confirming. An anonymous credit now flags the author's new anonymous_contributions profile setting (leaving display_name_preference at its full_name default) instead of being a no-op. The free-text state is dropped entirely: an Address needs a locality + city the export lacks, so it was only ever a warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Downloads the bytes at a URL and attaches them to ActiveStorage on the owner, reusing the same open-uri → attach flow as the retired resources:migrate_pdfs task, so imported stories can pull in their WordPress image URLs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The story importer sets anonymous_contributions for anonymous credits; give people the same control by exposing it next to the display-name preference so they can opt into (or out of) anonymous crediting on their own profile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turbo Drive discards a non-redirect 200 response to a form POST (logging "Form responses must redirect"), so the preview rendered server-side but never displayed in the browser. Render it with 422 like the app's other render-on-invalid actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both story_imports views set content_for(:page_bg_class) but weren't listed, so the alignment guard spec failed in CI. They're admin-only (StoryPolicy#import?), matching bg-blue-100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maebeale
force-pushed
the
maebeale/story-share-importer
branch
from
August 11, 2026 14:59
c5bda28 to
176343f
Compare
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.
🤖 suggested review level: 5 Inspect 🔬 external-CSV importer that creates Story/StoryIdea/Person/Comment/Affiliation/Scholarship records with real authorship, content conversion, and a row-by-row preview, plus a data-transforming seed reconciliation and namesake-funded grants
What is the goal of this PR and why is this important?
Importer — taxonomy translation
translationsmap: every distinct value inCategories,User Categories,Tags,who_is_your_story_about→ a Sector and/or typed Categories (age ranges, story populations, semantic overlaps). 100% value coverage verified.Importer — record model
publishedfrom the WordPress Status). A non-AWBW author (facilitator, not asuper_user) also gets a StoryIdea promoted into the story; AWBW-authored / author-less rows are Story-only.Person; unresolvable name (single name, "AWBW") is kept as a Comment (newhas_many :commentson Story + StoryIdea).anonymous_contributionsprofile setting (also a toggle on the profile edit form); otherwisedisplay_name_preferencesyncs from the credit unless already customized.\r\nparagraphs survive as HTML.created_at; exact-title Workshop link (else external title); featured flags carry over.Cathy scholarship/babs mayertags connect story → author → Scholarship → Grant (grants funded by their namesake Person).stateis dropped (an Address needs a locality + city the export lacks).Import preview interstitial
Seeds reconciled to the production portal
description(clean names, Adults18+), renamed in place; StoryPopulation age twins get a trailing underscore (reconciled first to dodge the name collision); 13 retired categories unpublished; positions match production. ACategoryDecoratorkeeps display clean. Verified fresh / idempotent / rename-in-place on the test DB.Also
AssetUrlImporter— downloads a remote file URL and attaches it as anAsset(for pulling in WordPress image URLs).Tests
Follow-ups
UI Testing Checklist