diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index de7d68b1f3..5aac647ab7 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -210,7 +210,7 @@ def set_organization :organization_status, :windows_type, :addresses, :categorizable_items, { comments: [ :created_by, :updated_by ] }, - affiliations: { person: [ :user, *Organization::PEOPLE_TAGGINGS ] } + affiliations: { person: [ :user, :affiliations, *Organization::PEOPLE_TAGGINGS ] } ).find(params[:id]) end diff --git a/app/decorators/organization_decorator.rb b/app/decorators/organization_decorator.rb index 742b838c0a..48ba7c90b8 100644 --- a/app/decorators/organization_decorator.rb +++ b/app/decorators/organization_decorator.rb @@ -235,6 +235,15 @@ def badges badges end + # Keeps every active title per person so a facilitator's card also lists e.g. "Director". + def active_affiliations_by_facilitator + object.affiliations + .select { |affiliation| affiliation.active? && affiliation.person } + .sort_by { |affiliation| [ affiliation.person.first_name.to_s.downcase, affiliation.person.last_name.to_s.downcase ] } + .group_by(&:person) + .select { |_person, affiliations| affiliations.any?(&:facilitator?) } + end + private # The facilitator rows that record real facilitation. A zero-length (start == diff --git a/app/views/bookmarks/_personal_row.html.erb b/app/views/bookmarks/_personal_row.html.erb index 16131eb60c..5d8c143474 100644 --- a/app/views/bookmarks/_personal_row.html.erb +++ b/app/views/bookmarks/_personal_row.html.erb @@ -57,8 +57,10 @@
<% author = bookmarkable.created_by if bookmarkable.respond_to?(:created_by) %> Created by: - <% if author&.person %> + <% if author&.person && allowed_to?(:show?, author.person) %> <%= person_profile_button(author.person, compact: true, width_class: "inline-flex w-fit", data: { turbo: false }) %> + <% elsif author&.person %> + <%= person_name(author.person) %> <% elsif author %> <%= user_button(author, data: { turbo: false }) %> <% elsif bookmark.bookmarkable_type == "Workshop" %> diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb index d24e9e716f..ef74d0904c 100644 --- a/app/views/organizations/show.html.erb +++ b/app/views/organizations/show.html.erb @@ -131,24 +131,27 @@
-

Affiliations

- <% active_affiliations = @organization.affiliations - .select { |a| !a.inactive? && (a.end_date.nil? || a.end_date >= Date.current) && a.person.present? } - .sort_by { |a| [a.person.first_name.to_s.downcase, a.person.last_name.to_s.downcase] } %> - <% grouped = active_affiliations.group_by(&:person) %> +

Facilitators

+ <% grouped = org_decorated.active_affiliations_by_facilitator.select { |person, _| allowed_to?(:show?, person) } %> <% if grouped.any? %>
<% grouped.each do |person, person_affiliations| %> -
+
<% titles = person_affiliations.filter_map { |a| a.title.presence }.uniq %> <% titles = titles.sort_by { |t| [t.downcase.include?("facilitator") ? 0 : 1, t.downcase] } %> <%= person_profile_button(person, subtitle: titles.any? ? titles.join(", ") : nil) %> + <% if current_user&.super_user? && !person.published? %> + + + + <% end %>
<% end %>
<% else %> -

No active affiliations.

+

No active facilitators.

<% end %>
diff --git a/db/seeds/features/2026-10-22-org-pages-list-active-facilitators.yml b/db/seeds/features/2026-10-22-org-pages-list-active-facilitators.yml new file mode 100644 index 0000000000..892b94d15c --- /dev/null +++ b/db/seeds/features/2026-10-22-org-pages-list-active-facilitators.yml @@ -0,0 +1,12 @@ +name: "Organization pages list active facilitators" +area: people +display_status: user_facing +released_on: 2026-10-22 +pr_number: 2785 +summary: >- + An organization page now lists its active facilitators, and facilitators see only the ones whose + profiles they can open, so every name leads to a profile. +pro_tips: + - "A facilitator's card still shows their other titles at the organization, like Director." + - "Admins see every active facilitator; a lock marks the ones hidden from facilitators, who don't see those cards." + - "On My bookmarks, a creator whose profile you can't open shows as a plain name." diff --git a/spec/decorators/organization_decorator_spec.rb b/spec/decorators/organization_decorator_spec.rb index 47911c9074..70e72ffe18 100644 --- a/spec/decorators/organization_decorator_spec.rb +++ b/spec/decorators/organization_decorator_spec.rb @@ -1,6 +1,26 @@ require "rails_helper" RSpec.describe OrganizationDecorator do + describe "#active_affiliations_by_facilitator" do + let(:organization) { create(:organization) } + let(:facilitator) { create(:person, first_name: "Zoe") } + let(:earlier) { create(:person, first_name: "Abe") } + + it "groups each active facilitator's active titles, sorted by name, leaving out everyone else" do + facilitation = create(:affiliation, organization: organization, person: facilitator) + director = create(:affiliation, organization: organization, person: facilitator, title: "Director") + create(:affiliation, organization: organization, person: facilitator, title: "Counselor", end_date: 1.month.ago) + create(:affiliation, organization: organization, person: earlier) + create(:affiliation, organization: organization, title: "Board Member") + create(:affiliation, organization: organization, end_date: 1.month.ago) + + grouped = organization.reload.decorate.active_affiliations_by_facilitator + + expect(grouped.keys).to eq([ earlier, facilitator ]) + expect(grouped[facilitator]).to contain_exactly(facilitation, director) + end + end + describe "#affiliated_since_display" do let(:organization) { create(:organization) } diff --git a/spec/requests/bookmarks_spec.rb b/spec/requests/bookmarks_spec.rb index 8630d2fdfc..b740debfe8 100644 --- a/spec/requests/bookmarks_spec.rb +++ b/spec/requests/bookmarks_spec.rb @@ -129,6 +129,27 @@ expect(response).to have_http_status(:ok) end + context "with the bookmarked item's creator" do + let(:creator) { create(:user, person: create(:person, first_name: "Cara", last_name: "Creator")) } + + before { workshop.update_columns(created_by_id: creator.id) } + + it "links the creator's profile when the viewer can open it" do + create(:affiliation, person: creator.person) + + get personal_bookmarks_path, headers: { "Turbo-Frame" => "personal_bookmarks_results" } + + expect(response.body).to include(person_path(creator.person)) + end + + it "shows the creator's name without a link when the viewer can't open the profile" do + get personal_bookmarks_path, headers: { "Turbo-Frame" => "personal_bookmarks_results" } + + expect(response.body).not_to include(person_path(creator.person)) + expect(response.body).to include("Cara") + end + end + it "can filter personal bookmarks by keyword via turbo frame" do get personal_bookmarks_path, params: { keyword: workshop.title }, headers: { "Turbo-Frame" => "personal_bookmarks_results" } diff --git a/spec/views/organizations/show.html.erb_spec.rb b/spec/views/organizations/show.html.erb_spec.rb index 28838201c9..23cc302aee 100644 --- a/spec/views/organizations/show.html.erb_spec.rb +++ b/spec/views/organizations/show.html.erb_spec.rb @@ -8,6 +8,7 @@ before do assign(:organization, organization) + sign_in user allow(view).to receive(:current_user).and_return(user) render end @@ -40,4 +41,43 @@ expect(rendered).to match(/Facilitator.*Director/) end end + + context "with people a viewer can't open" do + let(:facilitator) { create(:person, first_name: "Fay", last_name: "Active") } + let(:director) { create(:person, first_name: "Dee", last_name: "Director") } + let(:lapsed) { create(:person, first_name: "Lee", last_name: "Lapsed") } + let(:hidden) { create(:person, first_name: "Hal", last_name: "Hidden", profile_is_searchable: false) } + + before do + create(:affiliation, person: facilitator, organization: organization) + create(:affiliation, person: director, organization: organization, title: "Director") + create(:affiliation, person: lapsed, organization: organization, end_date: 1.month.ago) + create(:affiliation, person: hidden, organization: organization) + assign(:organization, organization.reload) + render + end + + it "lists only active facilitators whose profile the viewer can open" do + expect(rendered).not_to include("profile-hidden-lock") + expect(rendered).to include(person_path(facilitator)) + expect(rendered).not_to include(person_path(director)) + expect(rendered).not_to include(person_path(lapsed)) + expect(rendered).not_to include(person_path(hidden)) + end + + context "when the viewer is an admin" do + let(:user) { admin } + + it "lists every active facilitator, but not other titles" do + expect(rendered).to include(person_path(facilitator)) + expect(rendered).to include(person_path(hidden)) + expect(rendered).not_to include(person_path(director)) + expect(rendered).not_to include(person_path(lapsed)) + end + + it "locks only the facilitators others can't open" do + expect(rendered.scan('data-testid="profile-hidden-lock"').count).to eq(1) + end + end + end end