Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/controllers/admin/settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def setting_params
params.require(:setting).permit(:organization_id, :info_email, :reply_to_email, :invoice_prefix,
:return_address_id, :remittance_address_id, :contact_attention,
:ce_administrator_id, :ce_administrator_title, :import_user_id,
:participant_credit_person_id,
:ce_provider_number, :ce_accreditation_abbreviation,
:ce_accreditation_body, :ce_accreditation_url,
:programs_email, :no_reply_email,
Expand Down
8 changes: 5 additions & 3 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def hidden_filter_fields(params_hash)
end

# Byline for an AuthorCreditable record. Links each credited author to their person
# profile when the viewer can open it; anyone else renders as plain text. The
# profile when the viewer can open it; anyone else, and the participant stand-in
# (Setting.participant_credit_person_id), renders as plain text. The
# names and the separator come from the record (`credited_name_for_person`,
# `CREDIT_SEPARATOR`), so this matches the plain-text credit everywhere else. Falls
# back to the plain author_credit label when the credit resolves to nobody linkable
Expand All @@ -30,7 +31,8 @@ def credited_author_link(record, **link_options)

safe_join(people.map { |person|
name = record.credited_name_for_person(person)
allowed_to?(:show?, person) ? link_to(name, person_path(person), **link_options) : name
linkable = allowed_to?(:show?, person) && !person.participant_credit?
linkable ? link_to(name, person_path(person), **link_options) : name
}, AuthorCreditable::CREDIT_SEPARATOR)
end

Expand All @@ -43,7 +45,7 @@ def credited_author_link(record, **link_options)
# now also passes.
def credited_author_edit_button(record)
person = record.author_credit_person
return credited_author_link(record) unless person && allowed_to?(:manage?, person)
return credited_author_link(record) unless person && allowed_to?(:manage?, person) && !person.participant_credit?

person_edit_button(person,
display_name: record.author_credit,
Expand Down
4 changes: 4 additions & 0 deletions app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@ def placeholder_email?
PlaceholderEmail.match?(email)
end

def participant_credit?
persisted? && id == Setting.participant_credit_person_id
end

# The lowest numbered placeholder nobody with this name is using yet.
def next_free_placeholder_email
scope = Person.where("LOWER(first_name) = ? AND LOWER(last_name) = ?", first_name.downcase, last_name.downcase)
Expand Down
7 changes: 7 additions & 0 deletions app/models/setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Setting < ApplicationRecord
belongs_to :return_address, class_name: "Address", optional: true
belongs_to :remittance_address, class_name: "Address", optional: true
belongs_to :ce_administrator, class_name: "Person", optional: true
belongs_to :participant_credit_person, class_name: "Person", optional: true
belongs_to :import_user, class_name: "User", optional: true

# singleton is a constant true behind a unique index: MySQL has no CHECK-based
Expand Down Expand Up @@ -165,6 +166,12 @@ def self.import_user
stored_association(:import_user)
end

# The stand-in person credited on content by an unnamed participant. Their byline
# reads as a generic label, so it never links to a profile.
def self.participant_credit_person_id
stored(:participant_credit_person_id)
end

# The signatory's title, printed beside their name on the certificate.
def self.ce_administrator_title
stored(:ce_administrator_title) || DEFAULT_CE_ADMINISTRATOR_TITLE
Expand Down
16 changes: 16 additions & 0 deletions app/views/admin/settings/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@
input_html: { data: { controller: "remote-select", remote_select_model_value: SearchController::USER_ANY_STATUS_MODEL } } %>
</section>

<section class="rounded-xl border border-gray-200 bg-white p-6">
<h2 class="mb-1 text-lg font-semibold text-gray-800">Author credits</h2>
<p class="mb-4 text-sm text-gray-600">
A generic person credited on content by an unnamed participant. Like
"<%= AuthorCreditable::FACILITATOR_AUTHOR_LABEL %>", their name shows as plain text, not a profile link.
</p>
<% participant_credit_person = Setting.current.participant_credit_person %>
<%= f.input :participant_credit_person_id,
collection: participant_credit_person.present? ? [ [ participant_credit_person.remote_search_label[:label], participant_credit_person.id ] ] : [],
selected: participant_credit_person&.id,
prompt: "Search people",
label: "Participant stand-in",
hint: "e.g. an \"AWBW Participant\" person record",
input_html: { data: { controller: "remote-select", remote_select_model_value: "person" } } %>
</section>

<section class="rounded-xl border border-gray-200 bg-white p-6">
<h2 class="mb-4 text-lg font-semibold text-gray-800">Mailboxes</h2>
<%= f.input :info_email,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class AddParticipantCreditPersonToSettings < ActiveRecord::Migration[8.1]
def up
add_reference :settings, :participant_credit_person unless column_exists?(:settings, :participant_credit_person_id)
unless foreign_key_exists?(:settings, column: :participant_credit_person_id)
add_foreign_key :settings, :people, column: :participant_credit_person_id, on_delete: :nullify
end
end

def down
remove_foreign_key :settings, column: :participant_credit_person_id if foreign_key_exists?(:settings, column: :participant_credit_person_id)
remove_reference :settings, :participant_credit_person if column_exists?(:settings, :participant_credit_person_id)
end
end
5 changes: 4 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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_10_31_142321) do
ActiveRecord::Schema[8.1].define(version: 2026_11_03_122719) 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
Expand Down Expand Up @@ -2114,10 +2114,12 @@
t.string "ce_accreditation_abbreviation"
t.integer "import_user_id"
t.integer "home_featured_stories_limit"
t.bigint "participant_credit_person_id"
t.index ["ce_administrator_id"], name: "index_settings_on_ce_administrator_id"
t.index ["created_by_id"], name: "index_settings_on_created_by_id"
t.index ["import_user_id"], name: "index_settings_on_import_user_id"
t.index ["organization_id"], name: "index_settings_on_organization_id"
t.index ["participant_credit_person_id"], name: "index_settings_on_participant_credit_person_id"
t.index ["remittance_address_id"], name: "index_settings_on_remittance_address_id"
t.index ["return_address_id"], name: "index_settings_on_return_address_id"
t.index ["singleton"], name: "index_settings_on_singleton", unique: true
Expand Down Expand Up @@ -2951,6 +2953,7 @@
add_foreign_key "settings", "addresses", column: "return_address_id", on_delete: :nullify
add_foreign_key "settings", "organizations"
add_foreign_key "settings", "people", column: "ce_administrator_id", on_delete: :nullify
add_foreign_key "settings", "people", column: "participant_credit_person_id", on_delete: :nullify
add_foreign_key "settings", "users", column: "import_user_id", on_delete: :nullify
add_foreign_key "staff_aliases", "users"
add_foreign_key "staff_taggings", "staff_tags"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: "Participant stand-in bylines no longer link to a profile"
area: stories
display_status: admin_facing
released_on: 2026-11-03
action_path: "/admin/settings"
summary: >-
App settings has a new Author credits section where you pick the generic "AWBW Participant"
person. Content credited to them shows the name as plain text, like "AWBW Facilitator".
17 changes: 17 additions & 0 deletions spec/helpers/application_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ def strip_for(marker)
).at_css("a")
expect(link["data-turbo-frame"]).to eq("_top")
end

it "renders the participant stand-in as plain text, even to a viewer who can open it" do
Setting.create!(participant_credit_person: person)
allow(helper).to receive(:allowed_to?).with(:show?, person).and_return(true)
workshop = create(:workshop, author_credit_preference: "full_name")
allow(workshop).to receive(:author).and_return(person)

expect(helper.credited_author_link(workshop)).to eq("Ada Lovelace")
end
end

describe "#credited_author_edit_button" do
Expand Down Expand Up @@ -149,6 +158,14 @@ def strip_for(marker)

expect(helper.credited_author_edit_button(anonymous)).to eq("AWBW Staff")
end

it "stays plain text for the participant stand-in, even to an admin" do
Setting.create!(participant_credit_person: person)
allow(helper).to receive(:allowed_to?).with(:manage?, person).and_return(true)
allow(helper).to receive(:allowed_to?).with(:show?, person).and_return(true)

expect(helper.credited_author_edit_button(workshop)).to eq("Ada Lovelace")
end
end

describe "#timezone_visibility_hint" do
Expand Down
20 changes: 20 additions & 0 deletions spec/models/person_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,26 @@
end
end

describe "#participant_credit?" do
let(:person) { create(:person) }

it "is true for the participant stand-in chosen in settings" do
Setting.create!(participant_credit_person: person)

expect(person.participant_credit?).to be(true)
end

it "is false for everyone else" do
Setting.create!(participant_credit_person: create(:person))

expect(person.participant_credit?).to be(false)
end

it "is false when no stand-in is chosen" do
expect(person.participant_credit?).to be(false)
end
end

describe "#effective_author_credit_preference" do
let(:person) { build(:person, display_name_preference: "first_name_only") }

Expand Down
13 changes: 13 additions & 0 deletions spec/models/setting_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,19 @@ def stub_env(values)
end
end

describe ".participant_credit_person_id" do
it "is the id of the person the row points at" do
person = create(:person)
Setting.create!(participant_credit_person: person)

expect(described_class.participant_credit_person_id).to eq(person.id)
end

it "is blank until an admin picks someone" do
expect(described_class.participant_credit_person_id).to be_nil
end
end

describe ".import_user" do
it "is the user the row points at" do
user = create(:user)
Expand Down
19 changes: 19 additions & 0 deletions spec/requests/admin/settings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@
expect(options.find { |option| option["selected"] }.text).to include("imports@example.com")
end

it "offers the chosen participant stand-in as the picker's current choice" do
Setting.create!(participant_credit_person: create(:person, first_name: "AWBW", last_name: "Participant"))
sign_in admin

get admin_settings_path

options = Nokogiri::HTML(response.body).at_css("select[name='setting[participant_credit_person_id]']").css("option")
expect(options.find { |option| option["selected"] }.text).to include("AWBW Participant")
end

it "links the deployed build to what's merged on GitHub since" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("GIT_SHA").and_return("a1b2c3d4e5f6")
Expand Down Expand Up @@ -182,6 +192,15 @@
expect(Setting.import_user).to eq(user)
end

it "names the participant stand-in whose bylines never link" do
person = create(:person)

patch admin_settings_path, params: { setting: { participant_credit_person_id: person.id } }

expect(response).to have_http_status(:see_other)
expect(Setting.participant_credit_person_id).to eq(person.id)
end

it "saves the attention line shown on the contact page" do
patch admin_settings_path, params: { setting: { contact_attention: "Front Desk, Operations" } }

Expand Down
Loading