From 65381658262dfca6cf19b76add4d19e151c6fe81 Mon Sep 17 00:00:00 2001
From: Ryan Vasios
Date: Sun, 15 Mar 2026 16:12:28 -0400
Subject: [PATCH 1/5] Update controller endpoints
---
app/controllers/events_controller.rb | 39 +++++++++++++++++++++++++++-
1 file changed, 38 insertions(+), 1 deletion(-)
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb
index c043b3f52..6968f41e9 100644
--- a/app/controllers/events_controller.rb
+++ b/app/controllers/events_controller.rb
@@ -2,7 +2,7 @@ class EventsController < ApplicationController
include AhoyTracking, TagAssignable
skip_before_action :authenticate_user!, only: [ :index, :show ]
skip_before_action :verify_authenticity_token, only: [ :preview ]
- before_action :set_event, only: %i[ show edit update destroy preview manage copy_registration_form ]
+ before_action :set_event, only: %i[ show edit update destroy preview manage remind send_reminder copy_registration_form ]
def index
authorize!
@@ -59,6 +59,43 @@ def manage
end
end
+ def remind
+ authorize! @event, to: :remind?
+ @event = @event.decorate
+ @event_registrations = @event.event_registrations
+ .includes(:payments, registrant: [ :user, :contact_methods ])
+ .joins(:registrant)
+ .select { |r| r.registrant.preferred_email.present? }
+ @sample_registration = @event_registrations.first
+ @days_until_event = @event.start_date.present? ? (@event.start_date.to_date - Date.current).to_i : nil
+
+ if @sample_registration
+ mail = EventMailer.event_registration_reminder(@sample_registration, days_until_event: @days_until_event)
+ @reminder_preview_html = mail.html_part&.body&.decoded
+ end
+ end
+
+ def send_reminder
+ authorize! @event, to: :send_reminder?
+ allowed_ids = Array(params[:registration_ids]).map(&:to_i).reject(&:zero?)
+ registrations = @event.event_registrations
+ .where(id: allowed_ids)
+ .includes(registrant: [ :user, :contact_methods ])
+ .select { |r| r.registrant.preferred_email.present? }
+ days_until = @event.start_date.present? ? (@event.start_date.to_date - Date.current).to_i : nil
+
+ if registrations.empty?
+ redirect_to remind_event_path(@event), alert: "Please select at least one recipient."
+ return
+ end
+
+ registrations.each do |event_registration|
+ EventMailer.event_registration_reminder(event_registration, days_until_event: days_until).deliver_later
+ end
+
+ redirect_to manage_event_path(@event), notice: "Reminder emails are being sent to #{registrations.size} registrant#{'s' if registrations.size != 1}."
+ end
+
def create
authorize!
@event = Event.new(event_params)
From 38e3a29e2a33a245fa6577bc8991242a94de1342 Mon Sep 17 00:00:00 2001
From: Ryan Vasios
Date: Sun, 15 Mar 2026 16:26:56 -0400
Subject: [PATCH 2/5] Add routes, policy, views
---
app/mailers/event_mailer.rb | 22 +++++++
app/policies/event_policy.rb | 8 +++
.../event_registration_reminder.html.erb | 60 +++++++++++++++++++
.../event_registration_reminder.text.erb | 32 ++++++++++
app/views/events/manage.html.erb | 3 +
config/routes.rb | 2 +
test/mailers/previews/event_mailer_preview.rb | 5 ++
7 files changed, 132 insertions(+)
create mode 100644 app/views/event_mailer/event_registration_reminder.html.erb
create mode 100644 app/views/event_mailer/event_registration_reminder.text.erb
diff --git a/app/mailers/event_mailer.rb b/app/mailers/event_mailer.rb
index dd839a5f6..63e2b17b7 100644
--- a/app/mailers/event_mailer.rb
+++ b/app/mailers/event_mailer.rb
@@ -19,6 +19,28 @@ def event_registration_confirmation(event_registration)
)
end
+ def event_registration_reminder(event_registration, days_until_event: nil)
+ @event_registration = event_registration
+ @event = event_registration.event.decorate
+ @person = event_registration.registrant
+ @days_until_event = days_until_event
+
+ @notification_type = "Event registration reminder"
+
+ @time_zone = @person.user&.time_zone || Time.zone.name
+ @event_url = @event_registration.slug.present? ? event_url(@event, reg: @event_registration.slug) : event_url(@event)
+ @organization_name = ENV.fetch("ORGANIZATION_NAME", "AWBW")
+ @organization_website = ENV.fetch("ORGANIZATION_WEBSITE", root_url)
+
+ subject = "Reminder: #{@event.title} – #{@event.start_date.in_time_zone(@time_zone).strftime('%B %-d, %Y')}"
+ mail(
+ to: @person.preferred_email,
+ from: ENV.fetch("REPLY_TO_EMAIL", "no-reply@awbw.org"),
+ reply_to: ENV.fetch("REPLY_TO_EMAIL", "programs@awbw.org"),
+ subject: "AWBW Portal: #{subject}"
+ )
+ end
+
def event_registration_cancelled(event_registration)
@event_registration = event_registration
@event = event_registration.event.decorate
diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb
index 7b1f5d2a7..3d5fb2a72 100644
--- a/app/policies/event_policy.rb
+++ b/app/policies/event_policy.rb
@@ -33,6 +33,14 @@ def manage?
admin? || owner?
end
+ def remind?
+ manage?
+ end
+
+ def send_reminder?
+ manage?
+ end
+
alias_rule :preview?, to: :edit?
private
diff --git a/app/views/event_mailer/event_registration_reminder.html.erb b/app/views/event_mailer/event_registration_reminder.html.erb
new file mode 100644
index 000000000..1670c8f3c
--- /dev/null
+++ b/app/views/event_mailer/event_registration_reminder.html.erb
@@ -0,0 +1,60 @@
+Event reminder
+
+
+
+ Hello <%= @person.full_name %>,
+
+
+
+ This is a reminder that you're registered for the following <%= @organization_name %> event<%= raw(" " + (@days_until_event == 0 ? "today" : @days_until_event == 1 ? "tomorrow" : "in #{@days_until_event} days")) if @days_until_event.is_a?(Integer) %>:
+
+
+
+ <% 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) %><% } %>
+
+
+ <% if @event.labelled_cost.present? %>
+
+ <%= @event.labelled_cost %>
+
+ <% end %>
+
+ <% if @event.location.present? %>
+
+ <%= @event.location.name %>
+
+ <% end %>
+
+ <% if @event.videoconference_url.present? %>
+
+ Join us on <%= @event.decorate.videoconference_domain %>
+
+ <% end %>
+
+
+
+
+ Visit the event page for updates, directions, or calendar links:
+
+
+
+ <% if @event_registration.persisted? && @event_registration.slug.present? %>
+ View registration
+ <% end %>
+ View event
+
+
+
+ This is an automated reminder from <%= @organization_name %>.
+
diff --git a/app/views/event_mailer/event_registration_reminder.text.erb b/app/views/event_mailer/event_registration_reminder.text.erb
new file mode 100644
index 000000000..8de17315c
--- /dev/null
+++ b/app/views/event_mailer/event_registration_reminder.text.erb
@@ -0,0 +1,32 @@
+Event reminder
+
+Hello <%= @person.full_name %>,
+
+This is a reminder that you're registered for the following event<%= @days_until_event.is_a?(Integer) ? (@days_until_event == 0 ? " today" : @days_until_event == 1 ? " tomorrow" : " in #{@days_until_event} days") : "" %>:
+
+<% 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) %><% } %>
+
+<% if @event.location.present? %>
+Location: <%= @event.location.name %>
+<% end %>
+
+<% if @event.videoconference_url.present? %>
+Videoconference URL: <%= @event.videoconference_url %>
+<% end %>
+
+<% if @event.labelled_cost.present? %>
+<%= @event.labelled_cost %>
+<% end %>
+
+<% if @event_registration.slug.present? %>
+View your registration:
+<%= registration_ticket_url(@event_registration.slug) %>
+
+<% end %>View the event page:
+<%= @event_url %>
+
+--
+This is an automated reminder from <%= @organization_name %>.
diff --git a/app/views/events/manage.html.erb b/app/views/events/manage.html.erb
index 1ecfe73ae..ce28fa63f 100644
--- a/app/views/events/manage.html.erb
+++ b/app/views/events/manage.html.erb
@@ -11,6 +11,9 @@
+ <% if allowed_to?(:remind?, @event) %>
+ <%= link_to "Send reminder", remind_event_path(@event), class: "btn btn-secondary-outline btn-sm" %>
+ <% end %>
<%= link_to "Download CSV", manage_event_path(@event, format: :csv), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1", data: { turbo_frame: "_top" } %>
<%= link_to "View", event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<% if allowed_to?(:edit?, @event) %>
diff --git a/config/routes.rb b/config/routes.rb
index 695bde87f..122ae0b4e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -101,8 +101,10 @@
resources :events do
member do
get :manage
+ get :remind
patch :preview
post :copy_registration_form
+ post :send_reminder
end
resource :registrations, only: %i[ create destroy ], module: :events, as: :registrant_registration
resource :public_registration, only: [ :new, :create, :show ], module: :events
diff --git a/test/mailers/previews/event_mailer_preview.rb b/test/mailers/previews/event_mailer_preview.rb
index 09b4ae2d8..297ad51f5 100644
--- a/test/mailers/previews/event_mailer_preview.rb
+++ b/test/mailers/previews/event_mailer_preview.rb
@@ -4,6 +4,11 @@ def event_registration_confirmation
EventMailer.event_registration_confirmation(event_registration)
end
+ def event_registration_reminder
+ event_registration = sample_event_registration
+ EventMailer.event_registration_reminder(event_registration, days_until_event: 1)
+ end
+
def event_registration_cancelled
event_registration = sample_event_registration
event_registration.status = "cancelled"
From 73e9e01965996d8e9029c6d7547085f5bcb57201 Mon Sep 17 00:00:00 2001
From: Ryan Vasios
Date: Sun, 15 Mar 2026 16:33:58 -0400
Subject: [PATCH 3/5] Update event_mailer_spec
---
spec/mailers/event_mailer_spec.rb | 70 +++++++++++++++++++++++++++++++
1 file changed, 70 insertions(+)
diff --git a/spec/mailers/event_mailer_spec.rb b/spec/mailers/event_mailer_spec.rb
index 5ed83c5d2..4d4d31726 100644
--- a/spec/mailers/event_mailer_spec.rb
+++ b/spec/mailers/event_mailer_spec.rb
@@ -46,4 +46,74 @@
end
end
end
+
+ describe "#event_registration_reminder" do
+ let(:event_registration) { create(:event_registration) }
+ let(:mail) { described_class.event_registration_reminder(event_registration, days_until_event: days_until_event) }
+ let(:days_until_event) { 7 }
+
+ it "renders without raising" do
+ expect { mail.deliver_now }.not_to raise_error
+ end
+
+ it "sends to the registrant" do
+ expect(mail.to).to eq([ event_registration.registrant.preferred_email ])
+ end
+
+ it "includes the event title in the subject" do
+ expect(mail.subject).to include(event_registration.event.title)
+ end
+
+ it "includes the event title in the body" do
+ expect(mail.body.encoded).to include(event_registration.event.title)
+ end
+
+ it "includes the registrant name in the body" do
+ expect(mail.body.encoded).to include(event_registration.registrant.full_name)
+ end
+
+ it "includes reminder wording in the body" do
+ expect(mail.body.encoded).to include("This is a reminder that you're registered for the following")
+ end
+
+ context "when days_until_event is 0" do
+ let(:days_until_event) { 0 }
+
+ it "includes today in the body" do
+ expect(mail.body.encoded).to include("today")
+ end
+ end
+
+ context "when days_until_event is 1" do
+ let(:days_until_event) { 1 }
+
+ it "includes tomorrow in the body" do
+ expect(mail.body.encoded).to include("tomorrow")
+ end
+ end
+
+ context "when days_until_event is 7" do
+ let(:days_until_event) { 7 }
+
+ it "includes the number of days in the body" do
+ expect(mail.body.encoded).to include("7 days")
+ end
+ end
+
+ context "when days_until_event is nil" do
+ let(:days_until_event) { nil }
+ let(:mail) { described_class.event_registration_reminder(event_registration) }
+
+ it "renders without raising" do
+ expect { mail.deliver_now }.not_to raise_error
+ end
+
+ it "does not include today, tomorrow, or in N days in the body" do
+ body = mail.body.encoded
+ expect(body).not_to include("today")
+ expect(body).not_to include("tomorrow")
+ expect(body).not_to match(/\bin \d+ days\b/)
+ end
+ end
+ end
end
From e0030b84d3ac23da2d7347a87df8e7db207e71b8 Mon Sep 17 00:00:00 2001
From: Ryan Vasios
Date: Tue, 24 Mar 2026 17:48:21 -0400
Subject: [PATCH 4/5] Rename endpoint preview_reminder and add partial
---
app/controllers/events_controller.rb | 6 +-
app/policies/event_policy.rb | 2 +-
app/views/events/manage.html.erb | 4 +-
app/views/events/preview_reminder.html.erb | 77 ++++++++++++++++++++++
config/routes.rb | 2 +-
5 files changed, 83 insertions(+), 8 deletions(-)
create mode 100644 app/views/events/preview_reminder.html.erb
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb
index 6968f41e9..e4a9bff91 100644
--- a/app/controllers/events_controller.rb
+++ b/app/controllers/events_controller.rb
@@ -2,7 +2,7 @@ class EventsController < ApplicationController
include AhoyTracking, TagAssignable
skip_before_action :authenticate_user!, only: [ :index, :show ]
skip_before_action :verify_authenticity_token, only: [ :preview ]
- before_action :set_event, only: %i[ show edit update destroy preview manage remind send_reminder copy_registration_form ]
+ before_action :set_event, only: %i[ show edit update destroy preview manage preview_reminder send_reminder copy_registration_form ]
def index
authorize!
@@ -59,8 +59,8 @@ def manage
end
end
- def remind
- authorize! @event, to: :remind?
+ def preview_reminder
+ authorize! @event, to: :preview_reminder?
@event = @event.decorate
@event_registrations = @event.event_registrations
.includes(:payments, registrant: [ :user, :contact_methods ])
diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb
index 3d5fb2a72..8b76136db 100644
--- a/app/policies/event_policy.rb
+++ b/app/policies/event_policy.rb
@@ -33,7 +33,7 @@ def manage?
admin? || owner?
end
- def remind?
+ def preview_reminder?
manage?
end
diff --git a/app/views/events/manage.html.erb b/app/views/events/manage.html.erb
index ce28fa63f..87315499a 100644
--- a/app/views/events/manage.html.erb
+++ b/app/views/events/manage.html.erb
@@ -11,9 +11,7 @@
- <% if allowed_to?(:remind?, @event) %>
- <%= link_to "Send reminder", remind_event_path(@event), class: "btn btn-secondary-outline btn-sm" %>
- <% end %>
+ <%= link_to "Send reminder", preview_reminder_event_path(@event), class: "btn btn-secondary-outline btn-sm" %>
<%= link_to "Download CSV", manage_event_path(@event, format: :csv), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1", data: { turbo_frame: "_top" } %>
<%= link_to "View", event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<% if allowed_to?(:edit?, @event) %>
diff --git a/app/views/events/preview_reminder.html.erb b/app/views/events/preview_reminder.html.erb
new file mode 100644
index 000000000..624edc75c
--- /dev/null
+++ b/app/views/events/preview_reminder.html.erb
@@ -0,0 +1,77 @@
+<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
+
+
+ <%= link_to "← Manage registrants", manage_event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 mb-2 inline-block" %>
+
+ Send reminder email
+
+
+ <%= @event.title %>
+
+
+
+ <% if @event_registrations.empty? %>
+
There are no registrants with an email address to send a reminder to.
+ <%= link_to "Back to manage", manage_event_path(@event), class: "btn btn-secondary-outline" %>
+ <% else %>
+ <%= form_with url: send_reminder_event_path(@event), method: :post, local: true do |f| %>
+
Recipients
+
+ Choose who will receive this reminder:
+
+
+
+ <% if @reminder_preview_html.present? %>
+
+
Preview (sample registrant)
+
+
+ <%= raw @reminder_preview_html %>
+
+
+
+ <% end %>
+
+ <%= f.submit "Send reminder", class: "btn btn-primary", data: { turbo_confirm: "Send reminder emails to the selected registrant(s)?" } %>
+ <% end %>
+ <% end %>
+
diff --git a/config/routes.rb b/config/routes.rb
index 122ae0b4e..2bd2b8559 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -101,7 +101,7 @@
resources :events do
member do
get :manage
- get :remind
+ get :preview_reminder
patch :preview
post :copy_registration_form
post :send_reminder
From 8b8ba33e9c8c07af86a41e9dd6596795b2bcf774 Mon Sep 17 00:00:00 2001
From: Ryan Vasios
Date: Tue, 24 Mar 2026 17:50:33 -0400
Subject: [PATCH 5/5] Make reminder_preview link match other evnt_management
links
---
app/views/events/manage.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/events/manage.html.erb b/app/views/events/manage.html.erb
index 87315499a..6fea00da1 100644
--- a/app/views/events/manage.html.erb
+++ b/app/views/events/manage.html.erb
@@ -11,7 +11,7 @@
- <%= link_to "Send reminder", preview_reminder_event_path(@event), class: "btn btn-secondary-outline btn-sm" %>
+ <%= link_to "Send reminder", preview_reminder_event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<%= link_to "Download CSV", manage_event_path(@event, format: :csv), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1", data: { turbo_frame: "_top" } %>
<%= link_to "View", event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<% if allowed_to?(:edit?, @event) %>