Skip to content
Open
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
39 changes: 38 additions & 1 deletion app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 preview_reminder send_reminder copy_registration_form ]

def index
authorize!
Expand Down Expand Up @@ -59,6 +59,43 @@ def manage
end
end

def preview_reminder
authorize! @event, to: :preview_reminder?
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a nit. This will implicitly look up the policy name from the action. Should only need to use to: when a different policy is required. Sorry I missed this before.

Suggested change
authorize! @event, to: :preview_reminder?
authorize! @event

@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)
Expand Down
22 changes: 22 additions & 0 deletions app/mailers/event_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions app/policies/event_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def manage?
admin? || owner?
end

def preview_reminder?
manage?
end

def send_reminder?
manage?
end

alias_rule :preview?, to: :edit?

private
Expand Down
60 changes: 60 additions & 0 deletions app/views/event_mailer/event_registration_reminder.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<h1>Event reminder</h1>

<div style="margin-top: 36px; text-align: left;">
<p>
Hello <strong><%= @person.full_name %></strong>,
</p>

<p>
This is a reminder that you're registered for the following <%= @organization_name %> event<%= raw(" " + (@days_until_event == 0 ? "<strong>today</strong>" : @days_until_event == 1 ? "<strong>tomorrow</strong>" : "in <strong>#{@days_until_event} days</strong>")) if @days_until_event.is_a?(Integer) %>:
</p>

<div style="text-align: center; background-color: #f3f4f6; border-radius: 6px; padding: 24px; margin: 16px 0;">
<% if @event.respond_to?(:pre_title) && @event.pre_title.present? %>
<p style="font-size: 14px; font-weight: 600; color: #374151; margin: 0 0 4px; font-family: 'Telefon Bold', sans-serif;">
<%= @event.pre_title %>
</p>
<% end %>

<h2 style="font-size: 28px; font-weight: bold; color: #166534; margin: 0 0 16px; font-family: Lato, sans-serif;">
<%= @event.title %>
</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) %><% } %>
</p>

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

<% if @event.location.present? %>
<p style="font-size: 16px; color: #374151; margin: 0 0 8px;">
<%= @event.location.name %>
</p>
<% end %>

<% if @event.videoconference_url.present? %>
<p style="font-size: 16px; color: #374151; margin: 0 0 8px;">
<a href="<%= @event.videoconference_url %>" style="color: #374151; text-decoration: underline;">Join us on <%= @event.decorate.videoconference_domain %></a>
</p>
<% end %>
</div>
</div>

<p>
Visit the event page for updates, directions, or calendar links:
</p>

<p>
<% if @event_registration.persisted? && @event_registration.slug.present? %>
<a href="<%= registration_ticket_url(@event_registration.slug) %>" class="button">View registration</a>
<% end %>
<a href="<%= @event_url %>" class="button">View event</a>
</p>

<p style="margin-top: 24px; font-size: 12px; color: #6b7280;">
This is an automated reminder from <%= @organization_name %>.
</p>
32 changes: 32 additions & 0 deletions app/views/event_mailer/event_registration_reminder.text.erb
Original file line number Diff line number Diff line change
@@ -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 %>.
1 change: 1 addition & 0 deletions app/views/events/manage.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</p>
</div>
<div class="flex gap-2">
<%= 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) %>
Expand Down
77 changes: 77 additions & 0 deletions app/views/events/preview_reminder.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
<div class="max-w-4xl mx-auto <%= DomainTheme.bg_class_for(:events) %> border border-gray-200 rounded-xl shadow p-6">
<div class="mb-6">
<%= link_to "← Manage registrants", manage_event_path(@event), class: "text-sm text-gray-500 hover:text-gray-700 mb-2 inline-block" %>
<h1 class="text-2xl font-semibold text-gray-900">
Send reminder email
</h1>
<p class="text-gray-600 mt-1">
<%= @event.title %>
</p>
</div>

<% if @event_registrations.empty? %>
<p class="text-gray-600 mb-4">There are no registrants with an email address to send a reminder to.</p>
<%= 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| %>
<h2 class="text-lg font-semibold text-gray-800 mb-2 pb-2 border-b border-gray-200">Recipients</h2>
<p class="text-gray-600 mb-3">
Choose who will receive this reminder:
</p>
<div class="overflow-x-auto border border-gray-200 rounded-lg mb-6">
<table class="w-full border-collapse">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 w-10">
<input type="checkbox" id="recipients_select_all" class="rounded border-gray-300" checked aria-label="Select all" onclick="document.querySelectorAll('.recipient-checkbox').forEach(function(c){ c.checked = document.getElementById('recipients_select_all').checked })">
</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Name</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Email</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Payment status</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<% @event_registrations.each do |reg| %>
<% person = reg.registrant %>
<tr class="hover:bg-gray-50">
<td class="px-4 py-2">
<%= check_box_tag "registration_ids[]", reg.id, true, class: "recipient-checkbox rounded border-gray-300", id: "registration_ids_#{reg.id}" %>
</td>
<td class="px-4 py-2 text-sm text-gray-900"><%= person.full_name %></td>
<td class="px-4 py-2 text-sm text-gray-700"><%= person.preferred_email %></td>
<td class="px-4 py-2 text-sm">
<% if @event.object.cost_cents.to_i > 0 %>
<% if reg.scholarship_recipient? && reg.successful_payments_total_cents <= 0 %>
<span class="text-gray-400">—</span>
<% elsif reg.paid_in_full? %>
<span class="inline-flex items-center gap-1 rounded-full text-xs font-medium border px-2 py-0.5 bg-green-50 text-green-700 border-green-200">Paid</span>
<% else %>
<% due_cents = @event.object.cost_cents - reg.successful_payments_total_cents %>
<span class="inline-flex items-center gap-1 rounded-full text-xs font-medium border px-2 py-0.5 bg-amber-50 text-amber-700 border-amber-200">$<%= "%.2f" % (due_cents / 100.0) %> due</span>
<% end %>
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

<% if @reminder_preview_html.present? %>
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-3 pb-2 border-b border-gray-200">Preview (sample registrant)</h2>
<div class="border border-gray-200 rounded-lg bg-white overflow-hidden">
<div class="email-preview overflow-auto max-h-[70vh] p-4" style="font-family: Lato, sans-serif;">
<%= raw @reminder_preview_html %>
</div>
</div>
</div>
<% end %>

<%= f.submit "Send reminder", class: "btn btn-primary", data: { turbo_confirm: "Send reminder emails to the selected registrant(s)?" } %>
<% end %>
<% end %>
</div>
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@
resources :events do
member do
get :manage
get :preview_reminder
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
Expand Down
70 changes: 70 additions & 0 deletions spec/mailers/event_mailer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions test/mailers/previews/event_mailer_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading