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 %>
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 @@
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