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" } %>
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