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
7 changes: 7 additions & 0 deletions app/decorators/event_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ def detail(length: nil)
length ? description&.truncate(length) : description
end

# Compact event label for tight/tabular or multi-event contexts: the admin-set
# abbreviation (e.g. "TOS205") when present, otherwise the full title. Pair it
# with the full title as a tooltip so the abbreviation is never ambiguous.
def compact_label
object.abbreviation.presence || object.title
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: Single source of truth for the abbreviation-or-title fallback. Callers pair it with title="<full title>" so an unfamiliar abbreviation is never ambiguous.

# `show_videoconference_details` controls whether the join link/ID/passcode are
# carried into the calendar entry. Callers with a registration pass that
# registrant's gate (date + paid/intends); the default falls back to the
Expand Down
15 changes: 15 additions & 0 deletions app/decorators/organization_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ def facilitation_end_date
facilitator_affiliations.maximum(:end_date)
end

# In-memory program status (:new / :ongoing / :reinstated) for this org as it
# stood on a given date β€” the same New/Ongoing/Reinstate classification used in
# event context (Organization#facilitator_status_on), computed from the
# already-loaded affiliations so a profile can classify many events without an
# N+1. No facilitator affiliation starting before the date => :new; an earlier
# one still active on the date => :ongoing; all earlier ones ended => :reinstated.
def facilitator_status_as_of(date)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: Computed in-memory over the already-loaded affiliations (not the DB-backed Organization#facilitator_status_on) so classifying a page of ~9 events on the profile does not fire 2 queries per event.

reference = date&.to_date || Date.current
earlier = affiliations.select { |affiliation| affiliation.facilitator? && affiliation.start_date.present? && affiliation.start_date.to_date < reference }
return :new if earlier.empty?

active = earlier.any? { |affiliation| affiliation.end_date.nil? || affiliation.end_date.to_date >= reference }
active ? :ongoing : :reinstated
end

def badges
earliest = affiliations.minimum(:start_date) || start_date
years = earliest ? (Time.zone.now.year - earliest.year) : nil
Expand Down
1 change: 1 addition & 0 deletions app/policies/event_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def google_analytics?
:created_by_id,
:location_id,
:title,
:abbreviation,
:pre_title,
:videoconference_url,
:videoconference_label,
Expand Down
8 changes: 7 additions & 1 deletion app/views/events/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
"w-full rounded border-gray-300 shadow-sm px-3 py-2
focus:ring-blue-500 focus:border-blue-500",
} %>

<%= f.input :abbreviation,
label: "Abbreviation",
hint: "Short code shown on organization profiles (e.g. TOS205)",
input_html: {
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
placeholder: "e.g. TOS205"
} %>
<%= f.input :facilitator_training, as: :boolean, label: "Facilitator training event", wrapper_html: { class: "mb-0!" } %>
</div>

Expand Down
2 changes: 1 addition & 1 deletion app/views/events/background.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

<%# Title block %>
<div class="text-center mb-8">
<%= link_to @event.title, edit_event_path(@event), class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
<%= link_to @event.decorate.compact_label, edit_event_path(@event), title: @event.title, class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
<% if @event.start_date.present? %>
<p class="text-sm text-gray-500 mt-1"><%= @event.date_range %></p>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/events/dashboard.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<%# Centered title block %>
<div class="text-center mb-8">
<%= link_to @event.title, edit_event_path(@event), class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
<%= link_to @event.decorate.compact_label, edit_event_path(@event), title: @event.title, class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
<% if @event.start_date.present? %>
<p class="text-sm text-gray-500 mt-1">
<%= @event.date_range %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/events/revenue.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
<div class="min-w-0">
<div class="flex items-center gap-2">
<i class="fa-solid fa-chevron-down text-xs text-gray-400 transition-transform group-open/card:rotate-180"></i>
<span class="font-medium text-gray-900 truncate"><%= row.event.title %></span>
<span class="font-medium text-gray-900 truncate" title="<%= row.event.title %>"><%= row.event.compact_label %></span>
</div>
<div class="text-xs text-gray-500 pl-5"><%= row.event.date_range %></div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions app/views/organizations/sections/_events.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
<%= turbo_frame_tag "organization_events_section" do %>
<% if events.any? %>
<% if allowed_to?(:manage?, Organization) %>
<% decorated_org = organization.decorate %>
<div class="admin-only bg-blue-100 rounded-lg p-3 mb-6">
<p class="text-xs font-semibold text-gray-600 mb-2">Program status by event</p>
<div class="flex flex-wrap gap-2">
<% events.each do |event| %>
<% status = decorated_org.facilitator_status_as_of(event.start_date) %>
<%= link_to event_path(event),
title: event.title,
data: { turbo_frame: "_top" },
class: "inline-flex items-center rounded-full text-xs font-medium border px-2.5 py-0.5 #{OrganizationDecorator.program_status_classes(status)}" do %>
<%= status.to_s.titleize %> Β· <%= event.decorate.compact_label.truncate(24) %>
<% end %>
<% end %>
</div>
</div>
<% end %>

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 blur-on-submit">
<% events.each do |event| %>
<% decorated = event.decorate %>
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20260715041414_add_abbreviation_to_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddAbbreviationToEvents < ActiveRecord::Migration[8.1]
def up
add_column :events, :abbreviation, :string unless column_exists?(:events, :abbreviation)
end

def down
remove_column :events, :abbreviation, if_exists: true
end
end
1 change: 1 addition & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@
end

create_table "events", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.string "abbreviation"
t.boolean "autoshow_cost", default: true, null: false
t.boolean "autoshow_date", default: true, null: false
t.boolean "autoshow_location", default: true, null: false
Expand Down
12 changes: 12 additions & 0 deletions spec/decorators/event_decorator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@
end
end

describe "#compact_label" do
it "returns the abbreviation when present" do
event = build(:event, title: "Trauma-Informed Onsite", abbreviation: "TOS205").decorate
expect(event.compact_label).to eq("TOS205")
end

it "falls back to the full title when abbreviation is blank" do
event = build(:event, title: "Trauma-Informed Onsite", abbreviation: "").decorate
expect(event.compact_label).to eq("Trauma-Informed Onsite")
end
end

describe "#videoconference_room" do
it "pulls the Zoom meeting ID from the join URL and groups the digits" do
event = build(:event, videoconference_url: "https://awbw-org.zoom.us/j/88285411273").decorate
Expand Down
30 changes: 30 additions & 0 deletions spec/decorators/organization_decorator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@
end
end

describe "#facilitator_status_as_of" do
let(:organization) { create(:organization) }
let(:person) { create(:person) }
let(:reference) { Date.new(2026, 6, 1) }

it "is :new when there are no facilitator affiliations starting before the date" do
expect(organization.decorate.facilitator_status_as_of(reference)).to eq(:new)
end

it "ignores facilitator affiliations that start on or after the date" do
create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: reference)
expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:new)
end

it "is :ongoing when an earlier facilitator affiliation is still active on the date" do
create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: reference - 1.year, end_date: nil)
expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:ongoing)
end

it "is :reinstated when earlier facilitator affiliations all ended before the date" do
create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: reference - 2.years, end_date: reference - 1.year)
expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:reinstated)
end

it "ignores non-facilitator affiliations" do
create(:affiliation, organization: organization, person: person, title: "Volunteer", start_date: reference - 1.year, end_date: nil)
expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:new)
end
end

describe "#agency_type_option" do
it "returns a recognized type unchanged" do
organization = create(:organization, agency_type: "For-profit")
Expand Down
8 changes: 8 additions & 0 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ def offer_ce!(target_event)
expect(response.body).not_to include("Free open house")
end

it "labels an event by its abbreviation when set, keeping the full title as a tooltip" do
paid_training.update!(abbreviation: "TAC261")
sign_in admin
get revenue_events_path
expect(response.body).to include("TAC261")
expect(response.body).to include('title="TAC 261"')
end

it "returns to the originating event's dashboard when arrived from it" do
sign_in admin
get revenue_events_path(return_to: "dashboard", event_id: paid_training.id)
Expand Down
22 changes: 20 additions & 2 deletions spec/requests/organizations_events_section_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,31 @@ def register(event:, status: "registered")
end

it "lists each event once even when several members attended it" do
event = create(:event, title: "Shared Event")
event = create(:event, title: "Shared Event", abbreviation: "SE1")
register(event: event)
register(event: event)

get_events_section

expect(response.body.scan("Shared Event").size).to eq(1)
# One event card, and one deduped program-status chip (keyed by its
# abbreviation) β€” not one per registration. The card renders the title as a
# text node (">Shared Event"); the chip references it only in a title="…"
# tooltip, so the card count keys off the leading ">".
expect(response.body.scan(/>\s*Shared Event/).size).to eq(1)
expect(response.body.scan("SE1").size).to eq(1)
end

it "shows an admin program-status chip labeled with the event abbreviation" do
event = create(:event, title: "Trauma-Informed Onsite", abbreviation: "TOS205", start_date: 2.days.from_now)
person = create(:person)
create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: 1.year.ago, end_date: nil)
registration = create(:event_registration, registrant: person, event: event, status: "registered")
registration.event_registration_organizations.create!(organization: organization)

get_events_section

expect(response.body).to include("TOS205")
expect(response.body).to include("Ongoing")
end

it "renders the section heading and lazy frame on the profile page" do
Expand Down