Skip to content

Import WordPress stories: full taxonomy translation, portal-matched category seeds, legacy grants - #2163

Merged
maebeale merged 16 commits into
mainfrom
maebeale/story-share-importer
Aug 11, 2026
Merged

Import WordPress stories: full taxonomy translation, portal-matched category seeds, legacy grants#2163
maebeale merged 16 commits into
mainfrom
maebeale/story-share-importer

Conversation

@maebeale

@maebeale maebeale commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

🤖 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?

  • Migrate ~300 stories off the legacy WordPress site with no engineering, building faithful portal records (authorship, workshops, tags, provenance), and make the seeded taxonomy match production so imported tags land on real records.

Importer — taxonomy translation

  • A unified translations map: every distinct value in Categories, 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

  • Every row → a Story (published from the WordPress Status). A non-AWBW author (facilitator, not a super_user) also gets a StoryIdea promoted into the story; AWBW-authored / author-less rows are Story-only.
  • Author from the facilitator name → a Person; unresolvable name (single name, "AWBW") is kept as a Comment (new has_many :comments on Story + StoryIdea).
  • Anonymous credit flags the author's new anonymous_contributions profile setting (also a toggle on the profile edit form); otherwise display_name_preference syncs from the credit unless already customized.
  • Contentwpautop so the export's raw \r\n paragraphs survive as HTML.
  • Date → created_at; exact-title Workshop link (else external title); featured flags carry over.
  • Organizations resolve via an editable mapping file (variant/SKIP); non-AWBW authors get a facilitator Affiliation.
  • Grants: Cathy scholarship / babs mayer tags connect story → author → Scholarship → Grant (grants funded by their namesake Person).
  • state is dropped (an Address needs a locality + city the export lacks).

Import preview interstitial

  • The dry-run preview is now row-by-row: each row's shorthand, whether its organization/author are new or reused, whether it creates a Story (+ StoryIdea), the linked/external workshop, and the sectorable/categorizable items it would tag — so staff spot-check before confirming.

Seeds reconciled to the production portal

  • AgeRange range → description (clean names, Adults 18+), renamed in place; StoryPopulation age twins get a trailing underscore (reconciled first to dodge the name collision); 13 retired categories unpublished; positions match production. A CategoryDecorator keeps display clean. Verified fresh / idempotent / rename-in-place on the test DB.

Also

  • AssetUrlImporter — downloads a remote file URL and attaches it as an Asset (for pulling in WordPress image URLs).

Tests

  • Importer: 28 service + 13 request specs; Story/StoryIdea/Person + category/age-range/story-share suites green. All changed Ruby lint-clean. Dry-run against the real 309-row export: 305 stories / 259 ideas / 4 blank-title skips, no errors.

Follow-ups

  • Org mapping YAML ships with all 179 names blank — fill in variants/SKIP before the real run.

UI Testing Checklist

  • Admin Stories index shows an Import text link next to New Story
  • Uploading a WordPress export CSV shows the row-by-row preview and saves nothing
  • Confirming creates the records and redirects to Stories with a summary notice
  • Non-admins and signed-out users cannot reach the import screens
  • Age-range labels still read "Children (0-12)"; story chips never show a trailing underscore

Copilot AI lite review requested due to automatic review settings August 10, 2026 03:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 12:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 12:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale maebeale changed the title Import stories from a WordPress export CSV with an admin preview UI Import WordPress stories: full taxonomy translation, portal-matched category seeds, legacy grants Aug 10, 2026
Copilot AI review requested due to automatic review settings August 10, 2026 15:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 19:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 22:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 22:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale force-pushed the maebeale/story-share-importer branch from a8943de to 353ffa8 Compare August 11, 2026 07:06
Copilot AI review requested due to automatic review settings August 11, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 11, 2026 07:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale marked this pull request as ready for review August 11, 2026 14:51
Copilot AI review requested due to automatic review settings August 11, 2026 14:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

maebeale and others added 3 commits August 11, 2026 10:57
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>
maebeale and others added 13 commits August 11, 2026 10:57
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>
Copilot AI review requested due to automatic review settings August 11, 2026 14:59
@maebeale
maebeale force-pushed the maebeale/story-share-importer branch from c5bda28 to 176343f Compare August 11, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale merged commit fd3c594 into main Aug 11, 2026
3 checks passed
@maebeale
maebeale deleted the maebeale/story-share-importer branch August 11, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants