Make story author a direct person, separate from creator#1562
Open
maebeale wants to merge 1 commit into
Open
Conversation
Author credit on stories and story ideas was derived indirectly from the creating user's linked person, conflating "who wrote this" with "who entered it." This makes author a first-class Person association so authorship can be attributed independently of the account that created the record, while created_by/updated_by still capture the system user. - Add nullable author_id (FK to people) to stories and story_ideas, backfilled from each record's creator's person to preserve existing credits - AuthorCreditable now credits a direct author via a credited_person seam; workshop includers keep deriving it from the creator - Story credit preference defaults from the author's display preference, overridable per story - Rework the story form: a Person author picker (created_by becomes read-only), person dropdowns show email only to disambiguate duplicate names, and a linked "From Story Idea #N by ..." provenance line Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes “author” a first-class Person association on Story and StoryIdea, separating authorship from the account that created/updated the record while keeping created_by/updated_by for audit.
Changes:
- Add nullable
author_idFKs for stories and story ideas, and route author crediting through acredited_personseam. - Update story/story idea UI to display/select an author (
Person) and improve person dropdown labels (email only for duplicate names). - Add/adjust factories and specs to cover the new author behavior and defaulting credit preference from the author.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/support/shared_examples/author_creditable.rb | Updates shared examples to test direct-author vs creator-derived crediting. |
| spec/requests/stories_spec.rb | Ensures request specs create stories with explicit author. |
| spec/models/story_spec.rb | Adds coverage for defaulting credit preference from the author. |
| spec/helpers/person_helper_spec.rb | Adds tests for new person select option labeling behavior. |
| spec/factories/story_ideas.rb | Adds author association to story idea factory. |
| spec/factories/stories.rb | Adds author association to story factory. |
| db/schema.rb | Captures schema changes for new author_id columns/indexes/FKs (also includes unrelated constraint/index diffs). |
| db/migrate/20260605120000_add_author_to_stories_and_story_ideas.rb | Introduces author_id references + backfill from creator’s person. |
| app/views/story_ideas/show.html.erb | Switches “Story by” display to the direct author. |
| app/views/stories/show.html.erb | Switches “Story by” link to point at the direct author. |
| app/views/stories/_story_results.html.erb | Updates index table “Author” column to use story author. |
| app/views/stories/_form.html.erb | Adds author picker + reorganizes credit preference UI and story-idea provenance display. |
| app/models/story.rb | Adds belongs_to :author, credited-person override, and preference defaulting callback. |
| app/models/story_idea.rb | Adds belongs_to :author and credited-person override. |
| app/models/concerns/author_creditable.rb | Uses credited_person seam (defaulting to creator’s person). |
| app/helpers/person_helper.rb | Adds person_select_options to disambiguate duplicate names by email. |
| app/controllers/story_ideas_controller.rb | Sets author from current_user.person on create. |
| app/controllers/stories_controller.rb | Includes author in index scope, sets created_by on create, and carries author forward from story ideas. |
| @@ -0,0 +1,32 @@ | |||
| class AddAuthorToStoriesAndStoryIdeas < ActiveRecord::Migration[7.2] | |||
Comment on lines
+12
to
+16
| UPDATE stories | ||
| JOIN users ON users.id = stories.created_by_id | ||
| SET stories.author_id = users.person_id | ||
| WHERE users.person_id IS NOT NULL | ||
| SQL |
Comment on lines
+19
to
+23
| UPDATE story_ideas | ||
| JOIN users ON users.id = story_ideas.created_by_id | ||
| SET story_ideas.author_id = users.person_id | ||
| WHERE users.person_id IS NOT NULL | ||
| SQL |
Comment on lines
+101
to
+103
| def default_author_credit_preference | ||
| self.author_credit_preference ||= author&.display_name_preference | ||
| end |
Comment on lines
63
to
66
| def create | ||
| @story = Story.new(story_params.except(:category_ids, :sector_ids)) | ||
| @story.created_by = current_user | ||
| authorize! @story |
| class: "inline-block px-3 py-1 rounded-md text-sm font-medium #{DomainTheme.bg_class_for(:people, intensity: 100)} #{DomainTheme.text_class_for(:people)} #{DomainTheme.bg_class_for(:people, intensity: 100, hover: true)}" %> | ||
| <% else %> | ||
| <%= @story_idea.created_by&.name || "—" %> | ||
| <%= @story_idea.author&.full_name || "—" %> |
| </td> | ||
|
|
||
| <td class="px-4 py-2 text-sm text-gray-800"><%= story.created_by.name %></td> | ||
| <td class="px-4 py-2 text-sm text-gray-800"><%= story.author&.full_name || "—" %></td> |
Comment on lines
+276
to
+280
| <% if story_idea.author %> | ||
| <%= link_to story_idea.author_credit, person_path(story_idea.author), class: "font-medium text-gray-800 underline" %> | ||
| <% else %> | ||
| <span class="font-medium text-gray-800"><%= story_idea.author_credit %></span> | ||
| <% end %> |
Comment on lines
1338
to
1341
|
|
||
| add_foreign_key "action_text_mentions", "action_text_rich_texts" | ||
| add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" | ||
| add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" | ||
| add_foreign_key "affiliations", "organizations" | ||
| add_foreign_key "affiliations", "organizations", column: "organization_agency_id" |
Comment on lines
+258
to
+262
| collection: AuthorCreditable::ADMIN_FORM_OPTIONS, | ||
| prompt: "Select a preference", | ||
| selected: f.object.author_credit_preference || story_idea&.author_credit_preference || f.object.author&.display_name_preference, | ||
| label: "Author credit preference", | ||
| hint: "Defaults to the author's name display preference; controls how their name is shown", |
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.
What is the goal of this PR and why is this important?
Personassociation so authorship is attributed independently of the account that created the record, whilecreated_by/updated_bystill capture the system user.How did you approach the change?
author_id(FK topeople) onstoriesandstory_ideas, backfilled from each record's creator's person so existing credits are preserved.AuthorCreditablenow credits a directauthorvia acredited_personseam; workshop includers keep deriving it from the creator (no behavior change for them).UI Testing Checklist
Anything else to add?
authoris optional: rows whose creator has no linked person stay null and fall back to "Anonymous." Migration is reversible and backfill is idempotent.