Skip to content
Draft
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
20 changes: 11 additions & 9 deletions app/decorators/event_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ def display_image
end

def date
start_date.strftime("%B %d, %Y")
start_date.in_time_zone(event_zone).strftime("%B %d, %Y")
end

# Weekday-prefixed date range (e.g. "Thu-Fri, Jan 1-2, 2026") that collapses the
# year β€” and the month/weekday where possible β€” so nothing repeats unnecessarily.
def date_range
s = start_date.in_time_zone(Time.zone)
e = (end_date || start_date).in_time_zone(Time.zone)
s = start_date.in_time_zone(event_zone)
e = (end_date || start_date).in_time_zone(event_zone)
return s.strftime("%a, %b %-d, %Y") if s.to_date == e.to_date

if s.year == e.year && s.month == e.month
Expand All @@ -61,8 +61,8 @@ def date_range
# Same collapsed range as `date_range` but without the weekday prefix
# (e.g. "Sep 20-21, 2026") β€” for tighter contexts where the weekday is noise.
def short_date_range
s = start_date.in_time_zone(Time.zone)
e = (end_date || start_date).in_time_zone(Time.zone)
s = start_date.in_time_zone(event_zone)
e = (end_date || start_date).in_time_zone(event_zone)
return s.strftime("%b %-d, %Y") if s.to_date == e.to_date

if s.year == e.year && s.month == e.month
Expand Down Expand Up @@ -212,12 +212,14 @@ def videoconference_calendar_pending_note_html(portal_url, payment_pending: fals
# the "both days" notes in the registration details panel.
def multi_day?
return false unless start_date && end_date
start_date.in_time_zone(Time.zone).to_date != end_date.in_time_zone(Time.zone).to_date
start_date.in_time_zone(event_zone).to_date != end_date.in_time_zone(event_zone).to_date
end

def times(display_day: false, display_date: false, inline: false, styled: false)
s = start_date.in_time_zone(Time.zone)
e = (end_date || start_date).in_time_zone(Time.zone)
# Renders in the event's own zone by default; mailers pass `zone:` to show times
# in the recipient's zone instead.
def times(display_day: false, display_date: false, inline: false, styled: false, zone: event_zone)
s = start_date.in_time_zone(zone)
e = (end_date || start_date).in_time_zone(zone)
tz_abbr = s.strftime("%Z")
muted = styled ? "text-lg font-normal text-blue-400" : nil

Expand Down
24 changes: 12 additions & 12 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def default_reminder_message(days_until_event)
# reminder page (admins can edit it). Mirrors the mailer's fallback subject; the
# event date is event-level, resolved here in the app default time zone.
def default_reminder_subject(event)
date_suffix = event.start_date.present? ? " – #{event.start_date.in_time_zone.strftime('%B %-d, %Y')}" : ""
date_suffix = event.start_date.present? ? " – #{event.start_date.in_time_zone(event.event_zone).strftime('%B %-d, %Y')}" : ""
"AWBW Portal: Reminder: #{event.title}#{date_suffix}"
end

Expand Down Expand Up @@ -119,10 +119,10 @@ def form_header_uses_tokens?(form)
# Weekday-prefixed date for the registration details panel, without the year
# (the year lives in the page hero) β€” e.g. "Wednesday, August 12" or
# "Thursday-Friday, July 23-24". Nil when the event has no start date.
def event_dates_detail_label(event)
def event_dates_detail_label(event, zone: event&.event_zone)
return unless event&.start_date
s = event.start_date.in_time_zone(Time.zone)
e = (event.end_date || event.start_date).in_time_zone(Time.zone)
s = event.start_date.in_time_zone(zone)
e = (event.end_date || event.start_date).in_time_zone(zone)
return s.strftime("%A, %B %-d") if s.to_date == e.to_date
if s.year == e.year && s.month == e.month
"#{s.strftime("%A")}-#{e.strftime("%A")}, #{s.strftime("%B %-d")}-#{e.strftime("%-d")}"
Expand All @@ -135,8 +135,8 @@ def event_dates_detail_label(event)
# event show page's date line, or nil when the event has no start date.
def event_dates_label(event)
return unless event&.start_date
s = event.start_date.in_time_zone(Time.zone)
e = (event.end_date || event.start_date).in_time_zone(Time.zone)
s = event.start_date.in_time_zone(event.event_zone)
e = (event.end_date || event.start_date).in_time_zone(event.event_zone)
return s.strftime("%B %-d, %Y") if s.to_date == e.to_date
if s.year == e.year && s.month == e.month
"#{s.strftime("%B %-d")}-#{e.strftime("%-d")}, #{s.year}"
Expand All @@ -149,10 +149,10 @@ def event_dates_label(event)

# Event start-end time as plain text (e.g. "9 am - 4:30 pm PST"), mirroring the
# event show page's time formatting (minutes hidden when :00), or nil with no start.
def event_times_label(event)
def event_times_label(event, zone: event&.event_zone)
return unless event&.start_date
s = event.start_date.in_time_zone(Time.zone)
e = (event.end_date || event.start_date).in_time_zone(Time.zone)
s = event.start_date.in_time_zone(zone)
e = (event.end_date || event.start_date).in_time_zone(zone)
format = ->(d) do
t = d.strftime("%-l")
t += ":#{d.strftime("%M")}" unless d.strftime("%M") == "00"
Expand Down Expand Up @@ -197,7 +197,7 @@ def event_registration_close_label(event)
def event_registration_close_date_label(event)
close = event&.registration_close_date
return unless close
close.in_time_zone(Time.zone).strftime("%B %-d")
close.in_time_zone(event.event_zone).strftime("%B %-d")
end

# Just the time portion of the registration close, prefixed with "at" and
Expand All @@ -206,7 +206,7 @@ def event_registration_close_date_label(event)
def event_registration_close_time_label(event)
close = event&.registration_close_date
return unless close
local = close.in_time_zone(Time.zone)
local = close.in_time_zone(event.event_zone)
time = local.strftime("%-l")
time += ":#{local.strftime("%M")}" unless local.strftime("%M") == "00"
time += local.strftime("%P")
Expand All @@ -218,7 +218,7 @@ def event_registration_close_time_label(event)
# fall back to two days out at 9am.
def event_registration_close_default(event)
start = event&.start_date
base = start ? (start.in_time_zone(Time.zone) - 1.day).beginning_of_week(:monday) : 2.days.from_now
base = start ? (start.in_time_zone(event.event_zone) - 1.day).beginning_of_week(:monday) : 2.days.from_now
base.change(hour: 9, min: 0)
end

Expand Down
51 changes: 36 additions & 15 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Event < ApplicationRecord
# Validations
validates_presence_of :title, :start_date, :end_date
validates_inclusion_of :published, in: [ true, false ]
validate :time_zone_must_be_valid
validates_numericality_of :cost_cents, greater_than_or_equal_to: 0, allow_nil: true
validate :registration_form_required_when_publicly_registerable, on: :update
validate :staff_members_are_unique, on: :update
Expand All @@ -75,7 +76,7 @@ def self.remote_search(query)
end

def remote_search_label
label = start_date ? "#{title} (#{start_date.to_date.to_fs(:long)})" : title
label = start_date ? "#{title} (#{start_date.in_time_zone(event_zone).to_date.to_fs(:long)})" : title
{ id: id, label: label }
end

Expand Down Expand Up @@ -179,8 +180,8 @@ def registerable?
def day_count
return 1 if start_date.blank?

last_day = (end_date.presence || start_date).to_date
span = (last_day - start_date.to_date).to_i + 1
last_day = (end_date.presence || start_date).in_time_zone(event_zone).to_date
span = (last_day - start_date.in_time_zone(event_zone).to_date).to_i + 1
span.clamp(1, 5)
end

Expand All @@ -190,15 +191,15 @@ def time_title

# Like time_title but date only β€” no time or parens β€” for filter dropdowns.
def date_title
start_date ? "#{start_date.to_date.iso8601} β€” #{name}" : name
start_date ? "#{start_date.in_time_zone(event_zone).to_date.iso8601} β€” #{name}" : name
end

def full_name
"#{ name } (#{ start_text })"
end

def start_text
start_date.strftime("%Y-%m-%d @ %I:%M %p")
start_date.in_time_zone(event_zone).strftime("%Y-%m-%d @ %I:%M %p")
end

def name
Expand All @@ -212,33 +213,47 @@ def ce_hours_label
registration_ticket_callouts.find_by(builtin_key: "ce_hours")&.title.presence || "CE hours"
end

# Virtual attributes for date/time inputs (Firefox datetime-local compat)
# Default zone for events with no explicit time_zone β€” existing rows are left
# NULL (no backfill) and read as Pacific, and the form selector defaults here.
DEFAULT_TIME_ZONE = "Pacific Time (US & Canada)".freeze

# The event's own canonical zone β€” the times entered and displayed are pinned to
# it, so an event no longer floats to whoever's viewing. A blank/unrecognized
# column falls back to the default (Pacific).
def event_zone
ActiveSupport::TimeZone[time_zone.presence || DEFAULT_TIME_ZONE] ||
ActiveSupport::TimeZone[DEFAULT_TIME_ZONE]
end

# Virtual attributes for date/time inputs (Firefox datetime-local compat). Read
# and write in the event's own zone so the form round-trips wall-clock times
# regardless of the editing admin's session zone.
attr_writer :start_date_date, :start_date_time,
:end_date_date, :end_date_time,
:registration_close_date_date, :registration_close_date_time

def start_date_date
@start_date_date || start_date&.strftime("%Y-%m-%d")
@start_date_date || start_date&.in_time_zone(event_zone)&.strftime("%Y-%m-%d")
end

def start_date_time
@start_date_time || start_date&.strftime("%H:%M")
@start_date_time || start_date&.in_time_zone(event_zone)&.strftime("%H:%M")
end

def end_date_date
@end_date_date || end_date&.strftime("%Y-%m-%d")
@end_date_date || end_date&.in_time_zone(event_zone)&.strftime("%Y-%m-%d")
end

def end_date_time
@end_date_time || end_date&.strftime("%H:%M")
@end_date_time || end_date&.in_time_zone(event_zone)&.strftime("%H:%M")
end

def registration_close_date_date
@registration_close_date_date || registration_close_date&.strftime("%Y-%m-%d")
@registration_close_date_date || registration_close_date&.in_time_zone(event_zone)&.strftime("%Y-%m-%d")
end

def registration_close_date_time
@registration_close_date_time || registration_close_date&.strftime("%H:%M")
@registration_close_date_time || registration_close_date&.in_time_zone(event_zone)&.strftime("%H:%M")
end

# Virtual attribute for cost in dollars (converts to/from cost_cents)
Expand Down Expand Up @@ -315,9 +330,15 @@ def merge_date_time(field)

def build_datetime(date_str, time_str)
return nil if date_str.blank? && time_str.blank?
return Time.zone.parse(date_str) if date_str.present? && time_str.blank?
return Time.zone.parse("2000-01-01 #{time_str}") if date_str.blank? && time_str.present?
Time.zone.parse("#{date_str} #{time_str}")
return event_zone.parse(date_str) if date_str.present? && time_str.blank?
return event_zone.parse("2000-01-01 #{time_str}") if date_str.blank? && time_str.present?
event_zone.parse("#{date_str} #{time_str}")
end

def time_zone_must_be_valid
return if time_zone.blank? || ActiveSupport::TimeZone[time_zone]

errors.add(:time_zone, "is not a valid time zone")
end

def registration_form_required_when_publicly_registerable
Expand Down
2 changes: 1 addition & 1 deletion app/models/event_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ def self.search_by_params(params)
end

def name
"(#{ registrant&.full_name }) #{ event.start_date.strftime("%Y-%m-%d @ %I:%M %p") }: #{ event.title }"
"(#{ registrant&.full_name }) #{ event.start_date.in_time_zone(event.event_zone).strftime("%Y-%m-%d @ %I:%M %p") }: #{ event.title }"
end

# Email the communications box matches notifications against. Uniform accessor
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 @@ -160,6 +160,7 @@ def google_analytics?
:pre_date_text,
:facilitator_training,
:featured,
:time_zone,
:start_date, :start_date_date, :start_date_time,
:end_date, :end_date_date, :end_date_time,
:registration_close_date, :registration_close_date_date, :registration_close_date_time,
Expand Down
20 changes: 9 additions & 11 deletions app/views/event_mailer/_event_details_card.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@
<%= event.title %>
</h2>

<% Time.use_zone(time_zone) do %>
<% if event_dates_detail_label(event.object).present? %>
<p style="font-size: 22px; font-weight: bold; color: #1e3a8c; text-transform: uppercase; margin: 0 0 4px; font-family: Lato, sans-serif;">
<%= event_dates_detail_label(event.object) %>
</p>
<% end %>
<% if event_dates_detail_label(event.object, zone: time_zone).present? %>
<p style="font-size: 22px; font-weight: bold; color: #1e3a8c; text-transform: uppercase; margin: 0 0 4px; font-family: Lato, sans-serif;">
<%= event_dates_detail_label(event.object, zone: time_zone) %>
</p>
<% end %>

<% if event_times_label(event.object).present? %>
<p style="font-size: 18px; font-weight: bold; color: #1e3a8c; text-transform: uppercase; margin: 0 0 8px; font-family: Lato, sans-serif;">
<%= event_times_label(event.object) %>
</p>
<% end %>
<% if event_times_label(event.object, zone: time_zone).present? %>
<p style="font-size: 18px; font-weight: bold; color: #1e3a8c; text-transform: uppercase; margin: 0 0 8px; font-family: Lato, sans-serif;">
<%= event_times_label(event.object, zone: time_zone) %>
</p>
<% end %>

<% if event.labelled_cost.present? %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</h2>

<p style="font-size: 22px; font-weight: bold; color: #1e3a8c; text-transform: uppercase; margin: 0 0 8px; font-family: Lato, sans-serif;">
<% Time.use_zone(@time_zone) { %><%= @event.times(display_day: true, display_date: true) %><% } %>
<%= @event.times(display_day: true, display_date: true, zone: @time_zone) %>
</p>

<% if @event.location.present? %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Your registration for the following event has been cancelled:

<% if @event.respond_to?(:pre_title) && @event.pre_title.present? %><%= @event.pre_title %>
<% end %><%= @event.title %>
<% Time.use_zone(@time_zone) { %><%= @event.times(display_day: true, display_date: true) %><% } %>
<%= @event.times(display_day: true, display_date: true, zone: @time_zone) %>

<% if @event.location.present? %>
Location: <%= @event.location.name %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</h2>

<p style="font-size: 22px; font-weight: bold; color: #1e3a8c; text-transform: uppercase; margin: 0 0 8px; font-family: Lato, sans-serif;">
<% Time.use_zone(@time_zone) { %><%= @event.times(display_day: true, display_date: true) %><% } %>
<%= @event.times(display_day: true, display_date: true, zone: @time_zone) %>
</p>

<% if @event.labelled_cost.present? %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This message confirms your registration for the following event:

<% if @event.respond_to?(:pre_title) && @event.pre_title.present? %><%= @event.pre_title %>
<% end %><%= @event.title %>
<% Time.use_zone(@time_zone) { %><%= @event.times(display_day: true, display_date: true) %><% } %>
<%= @event.times(display_day: true, display_date: true, zone: @time_zone) %>

<% if @event.location.present? %>
Location: <%= @event.location.name %>
Expand Down
12 changes: 5 additions & 7 deletions app/views/event_mailer/event_registration_reminder.text.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ Event reminder Hello<%= @person.full_name %>,
<% if @event.respond_to?(:pre_title) && @event.pre_title.present? %>
<%= @event.pre_title %>
<% end %><%= @event.title %>
<% Time.use_zone(@time_zone) do %>
<% if event_dates_detail_label(@event.object).present? %>
<%= event_dates_detail_label(@event.object) %>
<% end %>
<% if event_dates_detail_label(@event.object, zone: @time_zone).present? %>
<%= event_dates_detail_label(@event.object, zone: @time_zone) %>
<% end %>

<% if event_times_label(@event.object).present? %>
<%= event_times_label(@event.object) %>
<% end %>
<% if event_times_label(@event.object, zone: @time_zone).present? %>
<%= event_times_label(@event.object, zone: @time_zone) %>
<% end %>
<% if event_location_label(@event.object).present? %>
Location:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Reminder content sent to each registrant:
<% if @custom_message.present? %><%= strip_tags(@custom_message).strip %>

<% end %><%= @event.title %>
<% Time.use_zone(@time_zone) do %><% if event_dates_detail_label(@event.object).present? %><%= event_dates_detail_label(@event.object) %>
<% end %><% if event_times_label(@event.object).present? %><%= event_times_label(@event.object) %>
<% end %><% end %><% if @event.labelled_cost.present? %>
<% if event_dates_detail_label(@event.object, zone: @time_zone).present? %><%= event_dates_detail_label(@event.object, zone: @time_zone) %>
<% end %><% if event_times_label(@event.object, zone: @time_zone).present? %><%= event_times_label(@event.object, zone: @time_zone) %>
<% end %><% if @event.labelled_cost.present? %>
<%= @event.labelled_cost %>
<% end %><% if event_location_label(@event.object).present? %>
Location: <%= event_location_label(@event.object) %>
Expand Down
13 changes: 12 additions & 1 deletion app/views/events/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<%# LEFT: Date/time, cost + pre-date text, videoconference + location %>
<div class="md:col-span-2 space-y-6">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-gray-200 bg-white px-3 py-2">
<label for="event_time_zone" class="flex items-center gap-1.5 text-sm text-gray-600">
<%= icon("fa-solid fa-earth-americas", class: "text-gray-400") %>
The times below are in
</label>
<%= f.time_zone_select :time_zone,
ActiveSupport::TimeZone.us_zones,
{ default: Event::DEFAULT_TIME_ZONE },
class: "min-w-0 flex-1 rounded border-gray-300 shadow-sm px-2 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500" %>
</div>

<div class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,13rem),1fr))] gap-3">
<div class="bg-white border border-gray-200 rounded-lg p-3">
<h3 class="font-medium mb-2">Start</h3>
Expand Down Expand Up @@ -225,7 +236,7 @@
<p class="text-sm text-gray-500 mb-3">Toggle which details appear on the event page.<br>Fields with no value will be ignored.</p>

<div class="space-y-2">
<% tz = Time.zone %>
<% tz = @event.event_zone %>
<% tz_abbr = @event.start_date.present? ? @event.start_date.in_time_zone(tz).strftime("%Z") : Time.current.strftime("%Z") %>

<%= f.input :autoshow_pre_date_text, as: :boolean, label: ("Pre-date text" + (@event.pre_date_text.present? ? " <span class='text-gray-400 font-normal'>#{h @event.pre_date_text}</span>" : "")).html_safe %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/events/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@
<% unless current_user && @event.actively_registered?(current_user.person) %>
<% if @event.autoshow_registration_close && @event.registration_close_date %>
<div class="text-base text-gray-700">
<% tz_abbr = @event.start_date.in_time_zone(Time.zone).strftime("%Z") %>
Registration closes <%= @event.registration_close_date.in_time_zone(Time.zone).strftime("%B %-d, %Y %l:%M %P") %> <%= tz_abbr %>
<% tz_abbr = @event.start_date.in_time_zone(@event.event_zone).strftime("%Z") %>
Registration closes <%= @event.registration_close_date.in_time_zone(@event.event_zone).strftime("%B %-d, %Y %l:%M %P") %> <%= tz_abbr %>
</div>
<% end %>
<% end %>
Expand Down
Loading
Loading