From b53e2d83b8fa0e72b1bf6dc41d41309276eb1324 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 5 Jun 2026 11:58:11 -0400 Subject: [PATCH] Make story author a direct person, separate from creator 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 --- app/controllers/stories_controller.rb | 12 +- app/controllers/story_ideas_controller.rb | 1 + app/helpers/person_helper.rb | 10 ++ app/models/concerns/author_creditable.rb | 8 +- app/models/story.rb | 15 ++- app/models/story_idea.rb | 5 + app/views/stories/_form.html.erb | 124 ++++++------------ app/views/stories/_story_results.html.erb | 2 +- app/views/stories/show.html.erb | 4 +- app/views/story_ideas/show.html.erb | 8 +- ...0_add_author_to_stories_and_story_ideas.rb | 32 +++++ db/schema.rb | 11 +- spec/factories/stories.rb | 1 + spec/factories/story_ideas.rb | 1 + spec/helpers/person_helper_spec.rb | 29 ++++ spec/models/story_spec.rb | 19 +++ spec/requests/stories_spec.rb | 2 + .../shared_examples/author_creditable.rb | 23 +++- 18 files changed, 200 insertions(+), 107 deletions(-) create mode 100644 db/migrate/20260605120000_add_author_to_stories_and_story_ideas.rb create mode 100644 spec/helpers/person_helper_spec.rb diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 96b30072d..95c320c72 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -9,7 +9,7 @@ def index if turbo_frame_request? per_page = params[:number_of_items_per_page].presence || 12 base_scope = authorized_scope(Story.includes(:windows_type, :organization, :workshop, - :created_by, :bookmarks, :primary_asset, + :author, :created_by, :bookmarks, :primary_asset, :story_idea)) filtered = base_scope.search_by_params(params) sortable = %w[title updated_at created_at windows_type workshop author organization] @@ -62,6 +62,7 @@ def edit def create @story = Story.new(story_params.except(:category_ids, :sector_ids)) + @story.created_by = current_user authorize! @story success = false @@ -127,11 +128,7 @@ def set_form_variables @story_idea = StoryIdea.find(params[:story_idea_id]) if params[:story_idea_id].present? @user = User.find(params[:user_id]) if params[:user_id].present? @organizations = authorized_scope(Organization.all, as: :affiliated).order(:name) - @story_ideas = authorized_scope(StoryIdea.includes(:created_by)) - .references(:users) - .order(:created_at) @people = Person.order(Arel.sql("LOWER(first_name), LOWER(last_name)")) - @users = User.has_access.includes(:person).left_joins(:person).order(Arel.sql("people.first_name IS NULL, LOWER(people.first_name), LOWER(people.last_name), LOWER(users.email)")) @windows_types = WindowsType.all @workshops = authorized_scope(Workshop.all).includes(:windows_type).order(:title) @categories_grouped = @@ -174,7 +171,7 @@ def apply_sort(scope, column, direction) scope.left_joins(:workshop) .reorder(Workshop.arel_table[:title].public_send(dir)) when "author" - scope.left_joins(created_by: :person) + scope.left_joins(:author) .reorder(Person.arel_table[:first_name].public_send(dir)) when "organization" scope.left_joins(:organization) @@ -189,7 +186,7 @@ def story_params params.require(:story).permit( :title, :rhino_body, :featured, :published, :publicly_visible, :publicly_featured, :youtube_url, :website_url, :windows_type_id, :organization_id, :workshop_id, :external_workshop_title, - :created_by_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id, :author_credit_preference, + :author_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id, :author_credit_preference, category_ids: [], sector_ids: [], primary_asset_attributes: [ :id, :file, :_destroy ], @@ -206,6 +203,7 @@ def set_story_attributes_from(idea) external_workshop_title: idea.external_workshop_title, windows_type_id: idea.windows_type_id, youtube_url: idea.youtube_url, + author_id: idea.author_id, author_credit_preference: idea.author_credit_preference } end diff --git a/app/controllers/story_ideas_controller.rb b/app/controllers/story_ideas_controller.rb index fae4b9508..973bbe21e 100644 --- a/app/controllers/story_ideas_controller.rb +++ b/app/controllers/story_ideas_controller.rb @@ -39,6 +39,7 @@ def create @story_idea = StoryIdea.new(story_idea_params.except(:category_ids, :sector_ids)) @story_idea.created_by = current_user @story_idea.updated_by = current_user + @story_idea.author = current_user.person authorize! @story_idea success = false diff --git a/app/helpers/person_helper.rb b/app/helpers/person_helper.rb index 74b6f124a..95683ced4 100644 --- a/app/helpers/person_helper.rb +++ b/app/helpers/person_helper.rb @@ -1,4 +1,14 @@ module PersonHelper + # Builds [ label, id ] pairs for a person select. Shows just the name, adding + # the email only to disambiguate people who share a full name. + def person_select_options(people) + duplicate_names = people.map(&:full_name).tally.select { |_, count| count > 1 }.keys.to_set + people.map do |person| + label = duplicate_names.include?(person.full_name) ? person.full_name_with_email : person.full_name + [ label, person.id ] + end + end + def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: nil, data: {}, inactive: false) if inactive bg = "bg-gray-100" diff --git a/app/models/concerns/author_creditable.rb b/app/models/concerns/author_creditable.rb index a702382be..5618aa946 100644 --- a/app/models/concerns/author_creditable.rb +++ b/app/models/concerns/author_creditable.rb @@ -20,7 +20,7 @@ module AuthorCreditable }.freeze def author_credit - person = created_by&.person + person = credited_person case author_credit_preference when "full_name" then person&.full_name || "Anonymous" when "first_name_last_initial" @@ -33,4 +33,10 @@ def author_credit else person&.name || "Anonymous" end end + + # The person credited as the author. Models with a direct author association + # (Story, StoryIdea) override this; the rest derive it from the creating user. + def credited_person + created_by&.person + end end diff --git a/app/models/story.rb b/app/models/story.rb index 7c479dc68..46e95f358 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -4,6 +4,7 @@ class Story < ApplicationRecord has_rich_text :rhino_body + belongs_to :author, class_name: "Person", optional: true belongs_to :created_by, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :windows_type @@ -36,6 +37,10 @@ class Story < ApplicationRecord validates :title, presence: true, uniqueness: true validates :rhino_body, presence: true + # Default the credit preference from the author's own name display preference, + # while leaving it overridable per story. + before_validation :default_author_credit_preference + # Nested attributes accepts_nested_attributes_for :primary_asset, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :gallery_assets, allow_destroy: true, reject_if: :all_blank @@ -48,7 +53,7 @@ class Story < ApplicationRecord attributes person_first: "people.first_name", person_last: "people.last_name" options :all, type: :text, default: true, default_operator: :or - scope { join_rich_texts.left_joins(created_by: :person) } + scope { join_rich_texts.left_joins(:author) } attributes action_text_body: "action_text_rich_texts.plain_text_body" options :action_text_body, type: :text, default: true, default_operator: :or end @@ -85,10 +90,18 @@ def self.search_by_params(params) stories end + def credited_person + author + end + def name title end + def default_author_credit_preference + self.author_credit_preference ||= author&.display_name_preference + end + def organization_name organization&.name end diff --git a/app/models/story_idea.rb b/app/models/story_idea.rb index 65155be0b..53d7702ee 100644 --- a/app/models/story_idea.rb +++ b/app/models/story_idea.rb @@ -14,6 +14,7 @@ def self.search_by_params(params) has_rich_text :rhino_body + belongs_to :author, class_name: "Person", optional: true belongs_to :created_by, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :organization @@ -48,6 +49,10 @@ def self.search_by_params(params) accepts_nested_attributes_for :primary_asset, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :gallery_assets, allow_destroy: true, reject_if: :all_blank + def credited_person + author + end + def name "StoryIdea ##{id}" end diff --git a/app/views/stories/_form.html.erb b/app/views/stories/_form.html.erb index 15fd78dd5..d4ee32998 100644 --- a/app/views/stories/_form.html.erb +++ b/app/views/stories/_form.html.erb @@ -205,21 +205,6 @@ <%= render "shared/form_image_fields", f: f, include_primary_asset: true %>
-
- <%= f.input :author_credit_preference, - as: :select, - collection: AuthorCreditable::ADMIN_FORM_OPTIONS, - prompt: "Select a preference", - selected: f.object.author_credit_preference || story_idea&.author_credit_preference, - label: "Author credit preference", - hint: "Controls how the author's name is displayed", - input_html: { - class: select_caret_class(blank: f.object.author_credit_preference.blank?), - style: custom_caret_style, - onchange: select_caret_onchange - } %> -
-
<%= f.input :youtube_url, as: :text, label: "YouTube URL", @@ -233,97 +218,74 @@ hint: "(open in external website instead)", input_html: { rows: 1, class: "w-full" } %>
-
- <% if story_idea %> -
-
- Story idea author credit: -
- <% if story_idea.created_by.person %> - <%= person_profile_button(story_idea.created_by.person, display_name: story_idea.author_credit, subtitle: story_idea.created_by.person.full_name) %> - <% else %> - <%= story_idea.author_credit %> - <% end %> -
- Story idea "author credit preference": -
-
<%= link_to story_idea.author_credit_preference, edit_story_idea_path(story_idea, anchor: "publish-preferences"), class: "hover:underline hover:text-blue-600" %>
-
- - <% if f.object.persisted? %> -
- Last updated by: -
- <%= f.object.updated_by&.full_name %> -
- <%= f.object.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %> -
- <% end %> -
- <% elsif f.object.persisted? %> -
- Last updated by: -
- <%= f.object.updated_by&.full_name %> -
- <%= f.object.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %> -
- <% end %> - -
- <%= f.input :created_by_id, - collection: @users.map { |u| [ u.full_name_with_email, u.id ] }, - prompt: "Select an author", - label: (f.object.created_by&.person ? ( - link_to "Story author", - person_path(f.object.created_by.person), - class: "hover:underline") : "Story author").html_safe, + <%= f.input :spotlighted_facilitator_id, + collection: person_select_options(@people), + prompt: "Select a facilitator", + label: (f.object.spotlighted_facilitator_id ? (link_to "Story spotlighted facilitator", + person_path(f.object.spotlighted_facilitator_id), + class: "hover:underline") : "Story spotlighted facilitator").html_safe, label_html: { class: "block font-medium mb-1 text-gray-700" }, input_html: { - class: select_caret_class(blank: f.object.created_by_id.blank?), + class: select_caret_class(blank: f.object.spotlighted_facilitator_id.blank?), style: custom_caret_style, onchange: select_caret_onchange } %>
+
+
- <%= f.input :spotlighted_facilitator_id, - collection: @people.map { |p| [ p.full_name_with_email, p.id ] }, - prompt: "Select a facilitator", - label: (f.object.spotlighted_facilitator_id ? (link_to "Story spotlighted facilitator", - person_path(f.object.spotlighted_facilitator_id), - class: "hover:underline") : "Story spotlighted facilitator").html_safe, + <%= f.input :author_id, + collection: person_select_options(@people), + prompt: "Select an author", + label: (f.object.author ? ( + link_to "Story author", + person_path(f.object.author), + class: "hover:underline") : "Story author").html_safe, label_html: { class: "block font-medium mb-1 text-gray-700" }, input_html: { - class: select_caret_class(blank: f.object.spotlighted_facilitator_id.blank?), + class: select_caret_class(blank: f.object.author_id.blank?), style: custom_caret_style, onchange: select_caret_onchange } %>
- <% story_idea_label = if f.object.story_idea - (link_to "Source story idea", - story_idea_path(f.object.story_idea), class: "hover:underline") - else - "Source story idea" - end %> - <%= f.input :story_idea_id, - collection: @story_ideas, label_method: :full_name, - value_method: :id, - selected: f.object.story_idea_id || @story_idea&.id, - label: story_idea_label.html_safe, - prompt: "Select idea", + <%= f.input :author_credit_preference, + as: :select, + 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", input_html: { - class: select_caret_class(blank: f.object.story_idea_id.blank?), + class: select_caret_class(blank: f.object.author_credit_preference.blank?), style: custom_caret_style, onchange: select_caret_onchange } %>
+ <% if story_idea %> +
+ From + <%= link_to "Story Idea # #{story_idea.id}", story_idea_path(story_idea), class: "font-medium text-gray-700 underline" %> + by + <% if story_idea.author %> + <%= link_to story_idea.author_credit, person_path(story_idea.author), class: "font-medium text-gray-800 underline" %> + <% else %> + <%= story_idea.author_credit %> + <% end %> + (preference "<%= link_to AuthorCreditable::ADMIN_FORM_OPTIONS.key(story_idea.author_credit_preference) || story_idea.author_credit_preference&.humanize, + edit_story_idea_path(story_idea, anchor: "publish-preferences"), + class: "italic underline hover:text-blue-600" %>") +
+ <% end %> + + <%= f.hidden_field :story_idea_id, value: f.object.story_idea_id || @story_idea&.id %> + <% if remaining_categories.any? %>
diff --git a/app/views/stories/_story_results.html.erb b/app/views/stories/_story_results.html.erb index 0a72f05b4..85419b665 100644 --- a/app/views/stories/_story_results.html.erb +++ b/app/views/stories/_story_results.html.erb @@ -62,7 +62,7 @@ <%= story.workshop&.title&.truncate(30) %> - <%= story.created_by.name %> + <%= story.author&.full_name || "—" %> <%= story.organization&.name&.truncate(30) %> diff --git a/app/views/stories/show.html.erb b/app/views/stories/show.html.erb index c2f3f28ee..460fb6da1 100644 --- a/app/views/stories/show.html.erb +++ b/app/views/stories/show.html.erb @@ -28,8 +28,8 @@

Story by: - <% if @story.created_by.person&.profile_is_searchable %> - <%= link_to @story.author_credit, person_path(@story.created_by) %> + <% if @story.author&.profile_is_searchable %> + <%= link_to @story.author_credit, person_path(@story.author) %> <% else %> <%= @story.author_credit %> <% end %> diff --git a/app/views/story_ideas/show.html.erb b/app/views/story_ideas/show.html.erb index c54ef8a99..fc5701c37 100644 --- a/app/views/story_ideas/show.html.erb +++ b/app/views/story_ideas/show.html.erb @@ -31,13 +31,13 @@

Story by: - <% if @story_idea.created_by&.person && allowed_to?(:show?, @story_idea.created_by.person) %> - <%= link_to @story_idea.created_by.name, - person_path(@story_idea.created_by.person), + <% if @story_idea.author && allowed_to?(:show?, @story_idea.author) %> + <%= link_to @story_idea.author.full_name, + person_path(@story_idea.author), data: { turbo_frame: "_top" }, 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 || "—" %> <% end %>
diff --git a/db/migrate/20260605120000_add_author_to_stories_and_story_ideas.rb b/db/migrate/20260605120000_add_author_to_stories_and_story_ideas.rb new file mode 100644 index 000000000..33d5a67b9 --- /dev/null +++ b/db/migrate/20260605120000_add_author_to_stories_and_story_ideas.rb @@ -0,0 +1,32 @@ +class AddAuthorToStoriesAndStoryIdeas < ActiveRecord::Migration[7.2] + def up + add_reference :stories, :author, type: :bigint, null: true, index: true + add_reference :story_ideas, :author, type: :bigint, null: true, index: true + + add_foreign_key :stories, :people, column: "author_id" + add_foreign_key :story_ideas, :people, column: "author_id" + + # Backfill author from the creating user's linked person, preserving existing + # author credits. Rows whose creator has no linked person stay null (Anonymous). + execute <<~SQL.squish + 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 + + execute <<~SQL.squish + 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 + end + + def down + remove_foreign_key :stories, column: "author_id" if foreign_key_exists?(:stories, column: "author_id") + remove_foreign_key :story_ideas, column: "author_id" if foreign_key_exists?(:story_ideas, column: "author_id") + remove_reference :stories, :author, if_exists: true + remove_reference :story_ideas, :author, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index e0f387ba6..2c5765eab 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_05_29_155200) do +ActiveRecord::Schema[8.1].define(version: 2026_06_05_120000) do create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.bigint "action_text_rich_text_id", null: false t.datetime "created_at", null: false @@ -18,7 +18,6 @@ t.string "mentionable_type", null: false t.datetime "updated_at", null: false t.index ["action_text_rich_text_id", "mentionable_type", "mentionable_id"], name: "index_at_mentions_on_rich_text_and_mentionable", unique: true - t.index ["action_text_rich_text_id"], name: "index_action_text_mentions_on_action_text_rich_text_id" t.index ["mentionable_type", "mentionable_id"], name: "index_action_text_mentions_on_mentionable" end @@ -899,6 +898,7 @@ create_table "stories", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "author_credit_preference" + t.bigint "author_id" t.text "body" t.datetime "created_at", null: false t.integer "created_by_id", null: false @@ -918,6 +918,7 @@ t.integer "windows_type_id", null: false t.integer "workshop_id" t.string "youtube_url" + t.index ["author_id"], name: "index_stories_on_author_id" t.index ["created_by_id"], name: "index_stories_on_created_by_id" t.index ["organization_id"], name: "index_stories_on_organization_id" t.index ["published"], name: "index_stories_on_published" @@ -930,6 +931,7 @@ create_table "story_ideas", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "author_credit_preference" + t.bigint "author_id" t.text "body" t.datetime "created_at", null: false t.integer "created_by_id", null: false @@ -942,6 +944,7 @@ t.integer "windows_type_id", null: false t.integer "workshop_id" t.string "youtube_url" + t.index ["author_id"], name: "index_story_ideas_on_author_id" t.index ["created_by_id"], name: "index_story_ideas_on_created_by_id" t.index ["organization_id"], name: "index_story_ideas_on_organization_id" t.index ["updated_by_id"], name: "index_story_ideas_on_updated_by_id" @@ -1333,8 +1336,6 @@ t.index ["year", "month"], name: "index_workshops_on_year_and_month" end - 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" @@ -1403,6 +1404,7 @@ add_foreign_key "resources", "workshops" add_foreign_key "sectorable_items", "sectors" add_foreign_key "stories", "organizations" + add_foreign_key "stories", "people", column: "author_id" add_foreign_key "stories", "people", column: "spotlighted_facilitator_id" add_foreign_key "stories", "story_ideas" add_foreign_key "stories", "users", column: "created_by_id" @@ -1410,6 +1412,7 @@ add_foreign_key "stories", "windows_types" add_foreign_key "stories", "workshops" add_foreign_key "story_ideas", "organizations" + add_foreign_key "story_ideas", "people", column: "author_id" add_foreign_key "story_ideas", "users", column: "created_by_id" add_foreign_key "story_ideas", "users", column: "updated_by_id" add_foreign_key "story_ideas", "windows_types" diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb index c2e0848bf..af1755af0 100644 --- a/spec/factories/stories.rb +++ b/spec/factories/stories.rb @@ -3,6 +3,7 @@ association :windows_type association :organization association :workshop + association :author, factory: :person association :created_by, factory: :user association :updated_by, factory: :user sequence(:title) { |n| "Story #{n}" } diff --git a/spec/factories/story_ideas.rb b/spec/factories/story_ideas.rb index 4d411b6c3..9adbc0cd2 100644 --- a/spec/factories/story_ideas.rb +++ b/spec/factories/story_ideas.rb @@ -7,6 +7,7 @@ rhino_body { "

My Body

" } permission_given { true } author_credit_preference { "full_name" } + association :author, factory: :person association :created_by, factory: :user association :updated_by, factory: :user diff --git a/spec/helpers/person_helper_spec.rb b/spec/helpers/person_helper_spec.rb new file mode 100644 index 000000000..dfc585144 --- /dev/null +++ b/spec/helpers/person_helper_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe PersonHelper, type: :helper do + describe "#person_select_options" do + it "shows only the name when the full name is unique" do + amy = create(:person, first_name: "Amy", last_name: "User", email: "amy@example.com") + ben = create(:person, first_name: "Ben", last_name: "Smith", email: "ben@example.com") + + options = helper.person_select_options([ amy, ben ]) + + expect(options).to contain_exactly([ "Amy User", amy.id ], [ "Ben Smith", ben.id ]) + end + + it "appends the email only for people who share a full name" do + amy_one = create(:person, first_name: "Amy", last_name: "User", email: "amy.one@example.com") + amy_two = create(:person, first_name: "Amy", last_name: "User", email: "amy.two@example.com") + ben = create(:person, first_name: "Ben", last_name: "Smith", email: "ben@example.com") + + options = helper.person_select_options([ amy_one, amy_two, ben ]) + + expect(options).to contain_exactly( + [ amy_one.full_name_with_email, amy_one.id ], + [ amy_two.full_name_with_email, amy_two.id ], + [ "Ben Smith", ben.id ] + ) + expect(amy_one.full_name_with_email).to include("(") + end + end +end diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index 36e94234e..a4b4453a1 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -3,6 +3,25 @@ RSpec.describe Story, type: :model do it_behaves_like "author_creditable", factory: :story + describe "author credit preference default" do + it "defaults a blank preference from the author's display name preference" do + author = create(:person, display_name_preference: "first_name_only") + story = create(:story, author: author, author_credit_preference: nil) + expect(story.author_credit_preference).to eq("first_name_only") + end + + it "does not override an explicitly set preference" do + author = create(:person, display_name_preference: "first_name_only") + story = create(:story, author: author, author_credit_preference: "anonymous") + expect(story.author_credit_preference).to eq("anonymous") + end + + it "leaves the preference blank when there is no author" do + story = create(:story, author: nil, author_credit_preference: nil) + expect(story.author_credit_preference).to be_nil + end + end + describe "#attach_assets_from_idea!" do let(:idea) { create(:story_idea) } let(:story) { create(:story, story_idea: idea) } diff --git a/spec/requests/stories_spec.rb b/spec/requests/stories_spec.rb index 2fbc28c11..e6c2e6251 100644 --- a/spec/requests/stories_spec.rb +++ b/spec/requests/stories_spec.rb @@ -124,6 +124,7 @@ windows_type: wt_adult, workshop: ws_art, organization: org_alpha, + author: author_alice.person, created_by: author_alice).tap do |s| s.update_columns(created_at: 3.days.ago, updated_at: 1.day.ago) end @@ -135,6 +136,7 @@ windows_type: wt_children, workshop: ws_music, organization: org_zulu, + author: author_zara.person, created_by: author_zara).tap do |s| s.update_columns(created_at: 1.day.ago, updated_at: 3.days.ago) end diff --git a/spec/support/shared_examples/author_creditable.rb b/spec/support/shared_examples/author_creditable.rb index 3ac1b03f8..7bcb0966a 100644 --- a/spec/support/shared_examples/author_creditable.rb +++ b/spec/support/shared_examples/author_creditable.rb @@ -1,8 +1,16 @@ RSpec.shared_examples "author_creditable" do |factory:| describe "#author_credit" do - let(:author_user) { create(:user, :with_person) } - let(:person) { author_user.person } - let(:record) { create(factory, created_by: author_user) } + let(:person) { create(:person) } + # Story/StoryIdea credit a direct author association; other includers derive + # the credited person from the creating user. + let(:has_author_association) { described_class.reflect_on_association(:author).present? } + let(:record) do + if has_author_association + create(factory, author: person) + else + create(factory, created_by: create(:user, person: person)) + end + end context "when author_credit_preference is full_name" do it "returns the person's full name" do @@ -46,10 +54,13 @@ end end - context "when user has no person" do + context "when there is no credited person" do it "returns Anonymous" do - user_without_person = create(:user, person: nil) - record.update!(created_by: user_without_person) + if has_author_association + record.update!(author: nil) + else + record.update!(created_by: create(:user, person: nil)) + end expect(record.author_credit).to eq("Anonymous") end end