From bd4015ce7cfc7167f730e279b3962c09ad7bcef0 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 21 Jun 2026 22:15:14 -0400 Subject: [PATCH 1/3] Derive organization status from affiliations; gate active on start date Builds on the affiliation-creation PR. Makes "is this org/person active" a function of affiliations rather than a manually maintained field, and distinguishes affiliations that have started from those still pending. - Affiliation.active now also requires start_date to have arrived; the broader active_or_pending (introduced in the prior PR) keeps future-dated affiliations. Mirrored by active? / active_or_pending? for in-memory filtering. - Reclassified every affiliation activeness check: forward-looking ones (org program-status, org active/published, the event dashboard's org list, the registration org snapshot, end-date/"ended" displays, people counts, non-admin affiliation visibility) use active_or_pending so an incoming facilitator counts; present-tense ones (person summary, "active facilitator" names, profile searchability) stay on the stricter active. - Organization status callback is now symmetric: activates an Inactive org that gains an active-or-pending affiliation and deactivates one that loses them, toggling only Active<->Inactive and leaving manual states alone. - Documented the bedrock "facilitators determine org status" rule and the two scopes in AGENTS.md. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 8 +++ app/controllers/organizations_controller.rb | 4 +- app/decorators/organization_decorator.rb | 4 +- app/decorators/person_decorator.rb | 4 +- app/models/affiliation.rb | 55 +++++++++++----- app/models/event_registration.rb | 2 +- app/models/organization.rb | 8 +-- app/policies/affiliation_policy.rb | 2 +- app/services/event_dashboard.rb | 6 +- app/views/organizations/_form.html.erb | 2 +- app/views/organizations/show.html.erb | 2 +- spec/models/affiliation_spec.rb | 73 +++++++++++++++++++++ spec/models/organization_spec.rb | 13 ++++ 13 files changed, 150 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index df764e7b9f..323349f73c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,6 +202,14 @@ end ### Affiliations +**Facilitator affiliations are bedrock**: an organization's standing with AWBW is *derived from its affiliations*, not a manually maintained field. The exact, case-sensitive title `"Facilitator"` (`Affiliation::FACILITATOR_TITLE`, matched by the `facilitators` scope / `facilitator?` predicate) is what counts; variants like "Lead Facilitator" don't. + +**Two activeness scopes** (mirrored by `active?` / `active_or_pending?` for preloaded records): +- `active_or_pending` — not inactive, not past its end date. **Includes future-dated** affiliations (a Facilitator dated to an upcoming training's month is "pending"). Use for forward-looking questions: org program-status (New/Ongoing/Reinstate), whether an org is active/published, the event dashboard's org list, end-date/"ended" displays, and de-dupe. +- `active` — `active_or_pending` **and** the start_date has arrived. Use for true present-tense state: a person's current-affiliations summary, "active facilitator" names, and profile searchability. + +`Affiliation#sync_organization_status_from_affiliations` (an `after_save`/`after_destroy` callback) keeps the org's `organization_status` in sync with whether it has any `active_or_pending` affiliations — symmetric (activates on gain, deactivates on loss) but only ever toggles between Active and Inactive, leaving manual states (Pending/Reinstate/Suspended/Unknown) alone. Tracked via Ahoy `autochange.organization`. + - `AffiliationServices::CreateFromRegistration` — On registration / org linking, creates a "job affiliation" with the typed title (when present) plus a standing "Facilitator" affiliation, in one transaction. Skips the facilitator one only when the person already has an active-or-pending affiliation titled exactly "Facilitator" with that org (a current one or one dated to a future training); an ended facilitator affiliation gets a fresh second one. Dedupe is by title + org + dates, so a job title like "Lead Facilitator" still gets its own Facilitator affiliation ### Notifications diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index d2a397df76..3f6ac8c658 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -14,13 +14,13 @@ def index )) filtered = base_scope.search_by_params(params).order(:name) @organizations_count = filtered.count - @active_people_count = Affiliation.active.where(organization_id: filtered.select(:id)).count("DISTINCT person_id, organization_id") + @active_people_count = Affiliation.active_or_pending.where(organization_id: filtered.select(:id)).count("DISTINCT person_id, organization_id") @organizations = filtered.paginate(page: params[:page], per_page: per_page) org_ids = @organizations.map(&:id) @affiliated_since = Affiliation.where(organization_id: org_ids) .group(:organization_id) .minimum(:start_date) - @active_people_counts = Affiliation.active + @active_people_counts = Affiliation.active_or_pending .where(organization_id: org_ids) .group(:organization_id) .distinct diff --git a/app/decorators/organization_decorator.rb b/app/decorators/organization_decorator.rb index ea2efce6b9..b3c93638ff 100644 --- a/app/decorators/organization_decorator.rb +++ b/app/decorators/organization_decorator.rb @@ -62,7 +62,7 @@ def affiliated_since_date end def affiliation_end_date - return nil if affiliations.active.exists? + return nil if affiliations.active_or_pending.exists? affiliations.maximum(:end_date) end @@ -72,7 +72,7 @@ def facilitator_since_date def facilitation_end_date facilitator_affiliations = affiliations.facilitators - return nil if facilitator_affiliations.active.exists? + return nil if facilitator_affiliations.active_or_pending.exists? facilitator_affiliations.maximum(:end_date) end diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 9a70cf7aa6..714c9f89ce 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -26,7 +26,7 @@ def default_display_image end def affiliation_end_date - return nil if affiliations.active.exists? + return nil if affiliations.active_or_pending.exists? affiliations.maximum(:end_date) end @@ -44,7 +44,7 @@ def active_facilitator_organization_names def facilitation_end_date facilitator_affiliations = affiliations.facilitators - return nil if facilitator_affiliations.active.exists? + return nil if facilitator_affiliations.active_or_pending.exists? facilitator_affiliations.maximum(:end_date) end diff --git a/app/models/affiliation.rb b/app/models/affiliation.rb index d5abec67f3..06f0bec5e4 100644 --- a/app/models/affiliation.rb +++ b/app/models/affiliation.rb @@ -21,7 +21,14 @@ class Affiliation < ApplicationRecord .where("affiliations.end_date IS NULL OR affiliations.end_date >= ?", Date.current) } - scope :active, -> { active_or_pending } + # Currently active: active-or-pending AND its start_date has arrived (or is + # unset). A future-dated affiliation (e.g. a Facilitator dated to an upcoming + # training's month) is therefore "pending" — counted by active_or_pending but + # not yet active. + scope :active, -> { + active_or_pending + .where("affiliations.start_date IS NULL OR affiliations.start_date <= ?", Date.current) + } # Only the exact, case-sensitive title "Facilitator" counts — variants like # "Lead Facilitator" or "facilitator" are deliberately excluded. BINARY forces @@ -31,9 +38,9 @@ class Affiliation < ApplicationRecord before_validation :skip_if_duplicate before_save :set_inactive_from_dates - after_save :deactivate_organization_if_no_active_people + after_save :sync_organization_status_from_affiliations after_save :sync_organization_affiliation_dates - after_destroy :deactivate_organization_if_no_active_people + after_destroy :sync_organization_status_from_affiliations after_destroy :sync_organization_affiliation_dates # Methods @@ -45,13 +52,20 @@ def facilitator? title.to_s.strip == "Facilitator" end - # Current: not flagged inactive and not past its end date. Mirrors the `active` - # scope so already-loaded affiliations can be filtered in Ruby without another - # query (e.g. on list pages that preload affiliations). - def active? + # Active-or-pending: not flagged inactive and not past its end date. A future + # start_date still counts (the affiliation is pending, not ended). Mirrors the + # `active_or_pending` scope for in-memory filtering of preloaded affiliations. + def active_or_pending? !inactive? && (end_date.nil? || end_date >= Date.current) end + # Currently active: active-or-pending AND its start_date has arrived. Mirrors + # the `active` scope so already-loaded affiliations can be filtered in Ruby + # without another query (e.g. on list pages that preload affiliations). + def active? + active_or_pending? && (start_date.nil? || start_date <= Date.current) + end + def name "#{person.name}" if person end @@ -93,7 +107,7 @@ def sync_organization_affiliation_dates affiliations = org.affiliations.where.not(id: destroyed_by_association ? id : nil) earliest_start = affiliations.minimum(:start_date) - has_active = affiliations.active.exists? + has_active = affiliations.active_or_pending.exists? updates = {} updates[:start_date] = earliest_start if org.start_date != earliest_start @@ -107,22 +121,31 @@ def sync_organization_affiliation_dates org.update_columns(updates) if updates.any? end - def deactivate_organization_if_no_active_people - return if organization.affiliations.active.exists? + # Keep the organization's Active/Inactive status in sync with whether it has any + # active-or-pending affiliations (an incoming facilitator counts). Symmetric: + # activates an Inactive org that gains people and deactivates one that loses + # them. Only ever toggles between Active and Inactive — manual states (Pending, + # Reinstate, Suspended, Unknown) are left untouched so they stick. + def sync_organization_status_from_affiliations + has_people = organization.affiliations.active_or_pending.exists? + current_name = organization.organization_status&.name + target_name = has_people ? "Active" : "Inactive" + + return if current_name == target_name + return unless [ "Active", "Inactive", nil ].include?(current_name) - inactive_status = OrganizationStatus.find_by(name: "Inactive") - return unless inactive_status - return if organization.organization_status_id == inactive_status.id + target_status = OrganizationStatus.find_by(name: target_name) + return unless target_status - organization.update_column(:organization_status_id, inactive_status.id) + organization.update_column(:organization_status_id, target_status.id) Ahoy::Tracker.new(user: Current.user).track( "autochange.organization", resource_type: "Organization", resource_id: organization.id, resource_title: organization.name, - change: "status_set_to_inactive", - reason: "no_active_affiliations" + change: has_people ? "status_set_to_active" : "status_set_to_inactive", + reason: has_people ? "active_affiliation_present" : "no_active_affiliations" ) end end diff --git a/app/models/event_registration.rb b/app/models/event_registration.rb index 63e0ca6ebb..70ea371353 100644 --- a/app/models/event_registration.rb +++ b/app/models/event_registration.rb @@ -398,7 +398,7 @@ def remote_search_label private def snapshot_registrant_organizations - registrant.affiliations.active.includes(:organization).find_each do |aff| + registrant.affiliations.active_or_pending.includes(:organization).find_each do |aff| event_registration_organizations.create(organization: aff.organization) end end diff --git a/app/models/organization.rb b/app/models/organization.rb index b1ed44097d..7c4d9876b9 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -60,7 +60,7 @@ class Organization < ApplicationRecord # See TagFilterable, Trendable, WindowsTypeFilterable scope :active, -> { status_active = joins(:organization_status).where(organization_statuses: { name: "Active" }) - affiliation_active = where(id: Affiliation.active.select(:organization_id)) + affiliation_active = where(id: Affiliation.active_or_pending.select(:organization_id)) status_active.or(affiliation_active) } scope :address, ->(address) do @@ -191,7 +191,7 @@ def program_location def program_status(recipient = nil) facilitators = affiliations.select(&:facilitator?) return "New" if facilitators.empty? - return "Reinstate" if facilitators.none?(&:active?) + return "Reinstate" if facilitators.none?(&:active_or_pending?) prior = recipient ? facilitators.reject { |a| a.person_id == recipient.id } : facilitators prior.any? ? "Ongoing" : "New" @@ -205,7 +205,7 @@ def program_status(recipient = nil) def self.program_statuses_by_id(org_ids) facilitator_scope = Affiliation.facilitators.where(organization_id: org_ids) with_facilitators = facilitator_scope.distinct.pluck(:organization_id).to_set - with_active = facilitator_scope.active.distinct.pluck(:organization_id).to_set + with_active = facilitator_scope.active_or_pending.distinct.pluck(:organization_id).to_set org_ids.index_with do |id| next :new if with_facilitators.exclude?(id) with_active.include?(id) ? :ongoing : :reinstated @@ -231,7 +231,7 @@ def organization_locality def published? # needed for my_bookmarks return true if organization_status&.name == "Active" - affiliations.active.exists? + affiliations.active_or_pending.exists? end def sector_list diff --git a/app/policies/affiliation_policy.rb b/app/policies/affiliation_policy.rb index 1f99ba9496..bfbc86200c 100644 --- a/app/policies/affiliation_policy.rb +++ b/app/policies/affiliation_policy.rb @@ -11,7 +11,7 @@ def destroy? relation_scope do |relation| next relation if admin? if authenticated? - relation.active + relation.active_or_pending end end end diff --git a/app/services/event_dashboard.rb b/app/services/event_dashboard.rb index b0f46af277..07e0a75060 100644 --- a/app/services/event_dashboard.rb +++ b/app/services/event_dashboard.rb @@ -307,7 +307,7 @@ def organization_registrant_ids_by_org .joins(:event_registration) .where(event_registration_id: active_registration_ids) .pluck(:organization_id, "event_registrations.registrant_id") - affiliated = Affiliation.active + affiliated = Affiliation.active_or_pending .where(person_id: registrant_ids) .pluck(:organization_id, :person_id) (snapshot + affiliated).each_with_object(Hash.new { |hash, key| hash[key] = Set.new }) do |(organization_id, person_id), map| @@ -663,7 +663,7 @@ def program_status_for(organization) # This event's active registrants' active affiliations, grouped by organization # id — the reference points for the program-status breakdown. def registrant_affiliations_by_org - @registrant_affiliations_by_org ||= Affiliation.active + @registrant_affiliations_by_org ||= Affiliation.active_or_pending .where(person_id: registrant_ids) .includes(:organization) .group_by(&:organization_id) @@ -779,7 +779,7 @@ def organization_ids snapshot_ids = EventRegistrationOrganization .where(event_registration_id: active_registration_ids) .pluck(:organization_id) - affiliated_ids = Affiliation.active + affiliated_ids = Affiliation.active_or_pending .where(person_id: registrant_ids) .pluck(:organization_id) (snapshot_ids + affiliated_ids).compact.uniq diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index f28a3f07cd..42a18432e5 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -254,7 +254,7 @@
<% org_earliest_aff = f.object.persisted? ? f.object.affiliations.minimum(:start_date) : nil %> - <% org_aff_ended = f.object.persisted? && f.object.affiliations.any? && !f.object.affiliations.active.exists? %> + <% org_aff_ended = f.object.persisted? && f.object.affiliations.any? && !f.object.affiliations.active_or_pending.exists? %> <% org_latest_end = f.object.persisted? ? f.object.affiliations.maximum(:end_date) : nil %> <% org_end_date = org_aff_ended ? org_latest_end : f.object.end_date %> <% org_decorated = f.object.decorate %> diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb index 9a34053787..f4b3fea011 100644 --- a/app/views/organizations/show.html.erb +++ b/app/views/organizations/show.html.erb @@ -54,7 +54,7 @@ <% end %> <% org_earliest_affiliation = @organization.affiliations.minimum(:start_date) %> <% org_affiliated_since = org_earliest_affiliation || @organization.start_date %> - <% org_show_aff_ended = @organization.affiliations.any? && !@organization.affiliations.active.exists? %> + <% org_show_aff_ended = @organization.affiliations.any? && !@organization.affiliations.active_or_pending.exists? %> <% org_show_end_date = org_show_aff_ended ? @organization.affiliations.maximum(:end_date) : @organization.end_date %> <% if org_affiliated_since.present? %>

diff --git a/spec/models/affiliation_spec.rb b/spec/models/affiliation_spec.rb index 9579fb1e25..c7f3379d74 100644 --- a/spec/models/affiliation_spec.rb +++ b/spec/models/affiliation_spec.rb @@ -94,6 +94,79 @@ described_class.active.joins(:organization).to_a }.not_to raise_error end + + it 'excludes a future-dated (pending) affiliation' do + pending_aff = create(:affiliation, inactive: false, start_date: 1.month.from_now) + expect(described_class.active).not_to include(pending_aff) + end + end + + describe '.active_or_pending' do + it 'includes a future-dated affiliation that .active excludes' do + pending_aff = create(:affiliation, inactive: false, start_date: 1.month.from_now) + + expect(described_class.active_or_pending).to include(pending_aff) + expect(described_class.active).not_to include(pending_aff) + end + + it 'excludes inactive and ended affiliations like .active does' do + inactive_aff = create(:affiliation, inactive: true) + ended_aff = create(:affiliation, inactive: false, end_date: 1.day.ago) + + expect(described_class.active_or_pending).not_to include(inactive_aff, ended_aff) + end + end + + describe '#active_or_pending?' do + it 'is true for a future start date (where #active? is false)' do + affiliation = build(:affiliation, inactive: false, start_date: 1.month.from_now) + + expect(affiliation.active_or_pending?).to be true + expect(affiliation.active?).to be false + end + + it 'is false once ended' do + expect(build(:affiliation, inactive: false, end_date: 1.day.ago).active_or_pending?).to be false + end + end + + describe 'organization status sync' do + let!(:active_status) { create(:organization_status, name: "Active") } + let!(:inactive_status) { create(:organization_status, name: "Inactive") } + + it 'reactivates an Inactive organization when it gains an active affiliation' do + org = create(:organization, organization_status: inactive_status) + + create(:affiliation, organization: org, inactive: false, end_date: nil) + + expect(org.reload.organization_status).to eq(active_status) + end + + it 'activates on a pending (future-dated) affiliation too' do + org = create(:organization, organization_status: inactive_status) + + create(:affiliation, organization: org, inactive: false, start_date: 1.month.from_now) + + expect(org.reload.organization_status).to eq(active_status) + end + + it 'deactivates an Active organization when its last affiliation ends' do + org = create(:organization, organization_status: active_status) + affiliation = create(:affiliation, organization: org, inactive: false, end_date: nil) + + affiliation.update!(end_date: 1.day.ago) + + expect(org.reload.organization_status).to eq(inactive_status) + end + + it 'leaves a manually set status (e.g. Suspended) untouched' do + suspended = create(:organization_status, name: "Suspended") + org = create(:organization, organization_status: suspended) + + create(:affiliation, organization: org, inactive: false, end_date: nil) + + expect(org.reload.organization_status).to eq(suspended) + end end describe '#facilitator?' do diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 75391fd1b2..abae190aed 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -336,6 +336,12 @@ it "is New when the org has no facilitator affiliations" do expect(org.program_status(recipient)).to eq("New") end + + it "is Ongoing (not Reinstate) when the org's only facilitator is pending (future-dated)" do + create(:affiliation, person: create(:person), organization: org, title: "Facilitator", start_date: 1.month.from_now) + + expect(org.reload.program_status(recipient)).to eq("Ongoing") + end end describe ".program_statuses_by_id" do @@ -363,5 +369,12 @@ expect(Organization.program_statuses_by_id([ org.id ])).to eq(org.id => :new) end + + it "treats a pending (future-dated) facilitator as ongoing, not reinstated" do + org = create(:organization) + create(:affiliation, organization: org, person: create(:person), title: "Facilitator", start_date: 1.month.from_now) + + expect(Organization.program_statuses_by_id([ org.id ])).to eq(org.id => :ongoing) + end end end From 01415e5841c1d135fdbfd9fbfbb0b068ff292b45 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 21 Jun 2026 22:44:00 -0400 Subject: [PATCH 2/3] Show pending affiliations only to admins Pending (future-dated) affiliations should be admin-only. The people index and org-profile affiliation lists now show active-or-pending only to super_users and the stricter active set to everyone else, replacing the hand-rolled inline filters with the active?/active_or_pending? predicates. Align AffiliationPolicy's (currently unused) authenticated scope to the same rule so it can't leak pending if it's ever wired up. Co-Authored-By: Claude Opus 4.8 --- app/policies/affiliation_policy.rb | 2 +- app/views/organizations/show.html.erb | 2 +- app/views/people/people_results.html.erb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/policies/affiliation_policy.rb b/app/policies/affiliation_policy.rb index bfbc86200c..1f99ba9496 100644 --- a/app/policies/affiliation_policy.rb +++ b/app/policies/affiliation_policy.rb @@ -11,7 +11,7 @@ def destroy? relation_scope do |relation| next relation if admin? if authenticated? - relation.active_or_pending + relation.active end end end diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb index f4b3fea011..33eeb3a754 100644 --- a/app/views/organizations/show.html.erb +++ b/app/views/organizations/show.html.erb @@ -120,7 +120,7 @@ Affiliations <% active_affiliations = @organization.affiliations - .select { |a| !a.inactive? && (a.end_date.nil? || a.end_date >= Date.current) && a.person.present? } + .select { |a| (current_user&.super_user? ? a.active_or_pending? : a.active?) && 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) %> <% if grouped.any? %> diff --git a/app/views/people/people_results.html.erb b/app/views/people/people_results.html.erb index 35c69f42cd..4c7e20348b 100644 --- a/app/views/people/people_results.html.erb +++ b/app/views/people/people_results.html.erb @@ -67,7 +67,7 @@ - <% affiliations = person.affiliations.select { |a| a.organization.present? && !a.inactive? && (a.end_date.nil? || a.end_date >= Date.current) } %> + <% affiliations = person.affiliations.select { |a| a.organization.present? && (current_user&.super_user? ? a.active_or_pending? : a.active?) } %> <% if affiliations.any? %>

<% affiliations.first(3).each do |affiliation| %> From 3b5f2599d2136b11f1780aa3351ecbc14f888de7 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 21 Jun 2026 22:53:25 -0400 Subject: [PATCH 3/3] Gate pending-affiliation visibility across people/org index & profile These four pages (people index, person profile, org index, org profile) are becoming non-admin-visible. Centralize the "pending affiliations are admin-only" rule in ApplicationController#include_pending_affiliations? and apply it to the remaining activeness decisions that were unconditionally active_or_pending and non-admin-visible: the org-index "active people" counts and the org-profile affiliations-ended date check. Refactor the two already-gated view filters onto the same helper so there's a single source of truth. Left as-is: the admin-only program-status column, the admin-only edit-form date decorators, and the person-show affiliations list (already strict active). Co-Authored-By: Claude Opus 4.8 --- app/controllers/organizations_controller.rb | 6 ++++-- app/helpers/application_helper.rb | 9 +++++++++ app/views/organizations/show.html.erb | 4 ++-- app/views/people/people_results.html.erb | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 3f6ac8c658..01d8b22972 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -13,14 +13,16 @@ def index logo_attachment: :blob )) filtered = base_scope.search_by_params(params).order(:name) + # Pending affiliations count toward "active people" only for admins. + people_scope = helpers.include_pending_affiliations? ? Affiliation.active_or_pending : Affiliation.active @organizations_count = filtered.count - @active_people_count = Affiliation.active_or_pending.where(organization_id: filtered.select(:id)).count("DISTINCT person_id, organization_id") + @active_people_count = people_scope.where(organization_id: filtered.select(:id)).count("DISTINCT person_id, organization_id") @organizations = filtered.paginate(page: params[:page], per_page: per_page) org_ids = @organizations.map(&:id) @affiliated_since = Affiliation.where(organization_id: org_ids) .group(:organization_id) .minimum(:start_date) - @active_people_counts = Affiliation.active_or_pending + @active_people_counts = people_scope .where(organization_id: org_ids) .group(:organization_id) .distinct diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5d84242875..cfcb7d2ff0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,13 @@ module ApplicationHelper + # Pending (future-dated) affiliations are visible only to admins; everyone else + # sees only the currently-active set. Single source of truth for the + # active vs. active_or_pending split on the people/organization index & profile + # pages, which are becoming visible to non-admins. In controllers, call via + # `helpers.include_pending_affiliations?`. + def include_pending_affiliations? + current_user&.super_user? + end + # Tags an admin may use in a form field name / group header that should # render (rather than escape) on the public form. Block + inline formatting, # links, line breaks, and font sizing/coloring (via or inline style). diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb index 33eeb3a754..e56d919531 100644 --- a/app/views/organizations/show.html.erb +++ b/app/views/organizations/show.html.erb @@ -54,7 +54,7 @@ <% end %> <% org_earliest_affiliation = @organization.affiliations.minimum(:start_date) %> <% org_affiliated_since = org_earliest_affiliation || @organization.start_date %> - <% org_show_aff_ended = @organization.affiliations.any? && !@organization.affiliations.active_or_pending.exists? %> + <% org_show_aff_ended = @organization.affiliations.any? && !(include_pending_affiliations? ? @organization.affiliations.active_or_pending : @organization.affiliations.active).exists? %> <% org_show_end_date = org_show_aff_ended ? @organization.affiliations.maximum(:end_date) : @organization.end_date %> <% if org_affiliated_since.present? %>

@@ -120,7 +120,7 @@ Affiliations <% active_affiliations = @organization.affiliations - .select { |a| (current_user&.super_user? ? a.active_or_pending? : a.active?) && a.person.present? } + .select { |a| (include_pending_affiliations? ? a.active_or_pending? : a.active?) && 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) %> <% if grouped.any? %> diff --git a/app/views/people/people_results.html.erb b/app/views/people/people_results.html.erb index 4c7e20348b..75cfb4122d 100644 --- a/app/views/people/people_results.html.erb +++ b/app/views/people/people_results.html.erb @@ -67,7 +67,7 @@ - <% affiliations = person.affiliations.select { |a| a.organization.present? && (current_user&.super_user? ? a.active_or_pending? : a.active?) } %> + <% affiliations = person.affiliations.select { |a| a.organization.present? && (include_pending_affiliations? ? a.active_or_pending? : a.active?) } %> <% if affiliations.any? %>

<% affiliations.first(3).each do |affiliation| %>