HOLD: Import stories from a WordPress export CSV with an admin preview UI#1856
HOLD: Import stories from a WordPress export CSV with an admin preview UI#1856maebeale wants to merge 2 commits into
Conversation
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>
| return redirect_to(new_story_import_path, alert: "That file is not a CSV.") unless csv?(file) | ||
|
|
||
| @filename = file.original_filename | ||
| @blob = ActiveStorage::Blob.create_and_upload!( |
There was a problem hiding this comment.
🤖 From Claude: The CSV is ~1MB, so it is stashed as an ActiveStorage blob between preview and confirm rather than the cookie session. The blob is purged in confirm once the import runs; an abandoned preview leaves an orphan blob — open to a periodic sweep if that matters.
| end | ||
|
|
||
| # Records that have no home in the current schema — logged, not dropped. | ||
| def note_unmapped_fields(row) |
There was a problem hiding this comment.
🤖 From Claude: These fields (original publish date, US state, free-text facilitator) have no home in the current schema, so they are logged as warnings instead of dropped silently — that surfaces the data-model gap in the preview. The permalink is deliberately not mapped to website_url, since that triggers an external redirect on Story#show.
|
|
||
| # Best-effort: tag whatever WordPress categories resolve to an existing | ||
| # Sector. Unmatched values are surfaced as warnings, never invented. | ||
| def tag_taxonomies(record, row) |
There was a problem hiding this comment.
🤖 From Claude: Best-effort: a WordPress category is tagged only when it resolves to an existing Sector (directly or via SECTOR_SYNONYMS). The ~131 thematic categories with no Sector match are warned, never invented — they would need a seeded StoryCategory taxonomy.
What is the goal of this PR and why is this important?
StoryIdea(the canonical submission record); rows that were public on WordPress also get a connected, publishedStory— mirroring the in-app idea→story promotion flow.HOLD:pending product decisions on the few unmappable fields (below) and a review of the import-user / visibility defaults before any real import is run.How did you approach the change?
StoryImporterdoes the mapping with no schema changes, logging fields that have no home rather than dropping them silently, and supports a dry run.rake import:stories[path,user_email]task and an admin-only UI from the Stories index — upload → dry-run preview of what will be created (ideas/stories/skipped/warnings) → confirm.AGENTS.mdupdated for the new service, rake task, and controller.UI Testing Checklist
Anything else to add?
state, the free-text facilitator, and the source permalink / post id.original_published_atandlegacy_wp_idcolumns (correct chronological sort + idempotent re-import — note the permalink can't go inwebsite_urlbecause that triggers an external redirect onStory#show), and seed aStoryCategorytaxonomy for the ~131 thematic WordPress categories that don't map onto an existingSector.