From f166fd224c9da8d690f3a72da93359a812e1114b Mon Sep 17 00:00:00 2001 From: maebeale Date: Thu, 4 Jun 2026 20:43:44 -0400 Subject: [PATCH] Allow all facilitators + free-text story author (#1514) The new/edit story form restricted the author dropdown to active, confirmed users, so facilitators who were inactive, never entered in the CMS, or only known by first name could not be credited. This mirrors the earlier fix to the spotlighted-facilitator dropdown. - Author dropdown now lists all users (drops the has_access filter), matching how the spotlighted-facilitator dropdown lists all people. - Add an optional free-text "author name" that overrides the dropdown and credit preference, for facilitators not in the CMS. - When a free-text author is given without selecting a user, default created_by to the current admin so the required association is set. - Suppress the author profile link on the show page when a free-text name is used so it doesn't link to the admin creator. Co-Authored-By: Claude Opus 4.8 --- app/controllers/stories_controller.rb | 7 ++++-- app/models/concerns/author_creditable.rb | 2 ++ app/views/stories/_form.html.erb | 6 +++++ app/views/stories/show.html.erb | 2 +- ...260604120000_add_author_name_to_stories.rb | 9 ++++++++ db/schema.rb | 3 ++- spec/models/story_spec.rb | 15 +++++++++++++ spec/requests/stories_spec.rb | 22 +++++++++++++++++++ 8 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20260604120000_add_author_name_to_stories.rb diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 96b30072d..31145fde7 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -62,6 +62,9 @@ def edit def create @story = Story.new(story_params.except(:category_ids, :sector_ids)) + # A free-text author name covers facilitators who aren't in the CMS, so an + # author User may not be selected. Fall back to the current admin as creator. + @story.created_by_id ||= current_user.id authorize! @story success = false @@ -131,7 +134,7 @@ def set_form_variables .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)")) + @users = User.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 = @@ -189,7 +192,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, + :created_by_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id, :author_credit_preference, :author_name, category_ids: [], sector_ids: [], primary_asset_attributes: [ :id, :file, :_destroy ], diff --git a/app/models/concerns/author_creditable.rb b/app/models/concerns/author_creditable.rb index a702382be..174677cb6 100644 --- a/app/models/concerns/author_creditable.rb +++ b/app/models/concerns/author_creditable.rb @@ -20,6 +20,8 @@ module AuthorCreditable }.freeze def author_credit + free_text = try(:author_name) + return free_text if free_text.present? person = created_by&.person case author_credit_preference when "full_name" then person&.full_name || "Anonymous" diff --git a/app/views/stories/_form.html.erb b/app/views/stories/_form.html.erb index 15fd78dd5..c206fb735 100644 --- a/app/views/stories/_form.html.erb +++ b/app/views/stories/_form.html.erb @@ -286,6 +286,12 @@ style: custom_caret_style, onchange: select_caret_onchange } %> + + <%= f.input :author_name, + as: :string, + label: "Or type an author name", + hint: "Use for facilitators not in the CMS, or where we only have a first name. Overrides the dropdown and credit preference.", + input_html: { class: "w-full px-3 py-2 border border-gray-300 rounded-lg" } %>
diff --git a/app/views/stories/show.html.erb b/app/views/stories/show.html.erb index c2f3f28ee..8d3817ba3 100644 --- a/app/views/stories/show.html.erb +++ b/app/views/stories/show.html.erb @@ -28,7 +28,7 @@

Story by: - <% if @story.created_by.person&.profile_is_searchable %> + <% if @story.author_name.blank? && @story.created_by.person&.profile_is_searchable %> <%= link_to @story.author_credit, person_path(@story.created_by) %> <% else %> <%= @story.author_credit %> diff --git a/db/migrate/20260604120000_add_author_name_to_stories.rb b/db/migrate/20260604120000_add_author_name_to_stories.rb new file mode 100644 index 000000000..2b44663dc --- /dev/null +++ b/db/migrate/20260604120000_add_author_name_to_stories.rb @@ -0,0 +1,9 @@ +class AddAuthorNameToStories < ActiveRecord::Migration[8.1] + def up + add_column :stories, :author_name, :string + end + + def down + remove_column :stories, :author_name, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index e0f387ba6..248ffe5ea 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_04_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 @@ -899,6 +899,7 @@ create_table "stories", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "author_credit_preference" + t.string "author_name" t.text "body" t.datetime "created_at", null: false t.integer "created_by_id", null: false diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index 36e94234e..13ffb8706 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -3,6 +3,21 @@ RSpec.describe Story, type: :model do it_behaves_like "author_creditable", factory: :story + describe "#author_credit with a free-text author_name" do + let(:author_user) { create(:user, :with_person) } + let(:story) { create(:story, created_by: author_user, author_credit_preference: "full_name") } + + it "returns the free-text name, overriding the created_by person and credit preference" do + story.update!(author_name: "Jane (first name only)") + expect(story.author_credit).to eq("Jane (first name only)") + end + + it "falls back to the created_by person when author_name is blank" do + story.update!(author_name: "") + expect(story.author_credit).to eq(author_user.person.full_name) + 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..2561b711e 100644 --- a/spec/requests/stories_spec.rb +++ b/spec/requests/stories_spec.rb @@ -211,6 +211,28 @@ def titles_in_response expect(response).to redirect_to(story_url(Story.last)) end + + it "creates a story with a free-text author name and no selected author" do + attributes = base_attributes.merge(author_name: "Unlisted Facilitator") + attributes.delete(:created_by_id) + + expect { + post stories_url, params: { story: attributes } + }.to change(Story, :count).by(1) + + story = Story.last + expect(story.author_name).to eq("Unlisted Facilitator") + expect(story.created_by).to eq(admin) + expect(story.author_credit).to eq("Unlisted Facilitator") + end + end + + describe "GET /new" do + it "includes inactive facilitators in the author dropdown" do + inactive_user = create(:user, :with_person, inactive: true) + get new_story_url + expect(response.body).to include(inactive_user.full_name_with_email) + end end end