Skip to content

Auto-scheduled monthly announcement #10975

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
21b483e
Add template column to announcement
Luke-Oldenburg Jul 16, 2025
ba17c29
Add monthly job
Luke-Oldenburg Jul 16, 2025
8f8d0bc
Fix job class name
Luke-Oldenburg Jul 16, 2025
d3668d6
Add monthly warning
Luke-Oldenburg Jul 16, 2025
abbb5fa
Add back frozen string literal
Luke-Oldenburg Jul 16, 2025
01e5d93
Discard other schema changes
Luke-Oldenburg Jul 16, 2025
0f34cfb
Update title for monthly
Luke-Oldenburg Jul 16, 2025
838ca03
Use find_each
Luke-Oldenburg Jul 16, 2025
227b9fa
Merge branch 'main' into scheduled-announcements
Luke-Oldenburg Jul 17, 2025
385a337
Rename column from `template` to `template_type`
garyhtou Jul 17, 2025
3a2b337
Handle errors from publishing announcement
garyhtou Jul 17, 2025
da1e8bd
[Announcement] Add Event::Config for `generate_monthly_announcement`
garyhtou Jul 17, 2025
4cba20b
Make monthly job use `generate_monthly_announcement`
garyhtou Jul 17, 2025
2741dbd
Refactor warning email
garyhtou Jul 17, 2025
0cf7d19
Rename template to template_type
Luke-Oldenburg Jul 17, 2025
534e835
Update app/views/announcement_mailer/monthly_warning.html.erb
Luke-Oldenburg Jul 17, 2025
23a151d
Use announcement created_at for donation summary
Luke-Oldenburg Jul 17, 2025
75ba96d
Rename monthly title
Luke-Oldenburg Jul 17, 2025
330cdc7
Update app/views/announcement_mailer/monthly_warning.html.erb
Luke-Oldenburg Jul 17, 2025
8a86da4
Add two day warning
Luke-Oldenburg Jul 17, 2025
5b0b87c
Delete monthly job
Luke-Oldenburg Jul 17, 2025
fce9a12
class.name -> name
Luke-Oldenburg Jul 17, 2025
3e2bc57
Rename before action method
garyhtou Jul 18, 2025
37646c5
Use `self.class.name` instead of `self.name`
garyhtou Jul 18, 2025
51cd76d
Revert "Delete monthly job"
Luke-Oldenburg Jul 18, 2025
f468a6a
Update mailer views
Luke-Oldenburg Jul 18, 2025
44ecabc
Merge branch 'main' into scheduled-announcements
Luke-Oldenburg Jul 21, 2025
5a357a0
Update app/models/announcement/templates/monthly.rb
Luke-Oldenburg Jul 21, 2025
67142cb
Merge branch 'main' into scheduled-announcements
Luke-Oldenburg Jul 21, 2025
1420c62
Merge branch 'main' into scheduled-announcements
Luke-Oldenburg Jul 21, 2025
4f70c1e
Remove .to_json
Luke-Oldenburg Jul 21, 2025
64e43a6
Add explanation
Luke-Oldenburg Jul 21, 2025
1526f03
Merge branch 'main' into scheduled-announcements
Luke-Oldenburg Jul 21, 2025
e79bc27
Fix render code
Luke-Oldenburg Jul 21, 2025
d39fba3
Add to params
Luke-Oldenburg Jul 21, 2025
072639c
Add toggle
Luke-Oldenburg Jul 21, 2025
a25d4a6
after_initialize -> after_create
Luke-Oldenburg Jul 21, 2025
8232f3f
Fix monthly
Luke-Oldenburg Jul 21, 2025
477dca8
Move explanation
Luke-Oldenburg Jul 21, 2025
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
6 changes: 4 additions & 2 deletions app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,8 @@ def event_params
:id,
:anonymous_donations,
:cover_donation_fees,
:contact_email
:contact_email,
:generate_monthly_announcement
]
)

Expand Down Expand Up @@ -1071,7 +1072,8 @@ def user_event_params
:id,
:anonymous_donations,
:cover_donation_fees,
:contact_email
:contact_email,
:generate_monthly_announcement
]
)

Expand Down
21 changes: 21 additions & 0 deletions app/jobs/announcement/monthly_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

class Announcement
class MonthlyJob < ApplicationJob
queue_as :default

def perform
Announcement.monthly_for(Date.today.prev_month).find_each do |announcement|
Rails.error.handle do
announcement.publish!
end
end

Event.includes(:config).where(config: { generate_monthly_announcement: true }).find_each do |event|
Announcement::Templates::Monthly.new(event:, author: User.system_user).create
end
end

end

end
15 changes: 15 additions & 0 deletions app/jobs/announcement/seven_day_warning_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class Announcement
class SevenDayWarningJob < ApplicationJob
queue_as :low

def perform
Announcement.monthly_for(Date.today).where.not(aasm_state: :published).find_each do |announcement|
AnnouncementMailer.with(announcement:).seven_day_warning.deliver_now
end
end

end

end
15 changes: 15 additions & 0 deletions app/jobs/announcement/two_day_warning_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class Announcement
class TwoDayWarningJob < ApplicationJob
queue_as :low

def perform
Announcement.monthly_for(Date.today).where(aasm_state: :template_draft).find_each do |announcement|
AnnouncementMailer.with(announcement:).two_day_warning.deliver_now
end
end

end

end
20 changes: 20 additions & 0 deletions app/mailers/announcement_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
# frozen_string_literal: true

class AnnouncementMailer < ApplicationMailer
before_action :set_warning_variables, only: [:seven_day_warning, :two_day_warning]

def announcement_published
@announcement = params[:announcement]
@event = @announcement.event

mail to: params[:email], subject: "#{@announcement.title} | #{@event.name}", from: hcb_email_with_name_of(@event)
end

def seven_day_warning
mail to: @emails, subject: "[#{@event.name}] Your scheduled monthly announcement will be delivered on #{@scheduled_for.strftime("%b %-m")}"
end

def two_day_warning
mail to: @emails, subject: "[#{@event.name}] Your scheduled monthly announcement will be delivered on #{@scheduled_for.strftime("%b %-m")}"
end

def set_warning_variables
@announcement = params[:announcement]
@event = @announcement.event

@emails = @event.managers.map(&:email_address_with_name)
@emails << @event.config.contact_email if @event.config.contact_email.present?

@scheduled_for = Date.today.next_month.beginning_of_month
end

end
4 changes: 4 additions & 0 deletions app/models/announcement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# published_at :datetime
# rendered_email_html :text
# rendered_html :text
# template_type :string
# title :string not null
# created_at :datetime not null
# updated_at :datetime not null
Expand Down Expand Up @@ -55,6 +56,9 @@ class Announcement < ApplicationRecord
end
end

scope :saved, -> { where.not(aasm_state: :template_draft) }
scope :monthly, -> { where(template_type: Announcement::Templates::Monthly.name) }
scope :monthly_for, ->(date) { monthly.where("announcements.created_at BETWEEN ? AND ?", date.beginning_of_month, date.end_of_month) }
validate :content_is_json

scope :saved, -> { where.not(aasm_state: :template_draft).where.not(content: {}) }
Expand Down
2 changes: 1 addition & 1 deletion app/models/announcement/templates/blank.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def json_content
end

def create
Announcement.create!(event: @event, title:, content: json_content, aasm_state: :template_draft, author: @author)
Announcement.create!(event: @event, title:, content: json_content, aasm_state: :template_draft, author: @author, template_type: self.class.name)
end

end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def json_content
end

def create
Announcement.create!(event: @event, title:, content: json_content, aasm_state: :template_draft, author: @author)
Announcement.create!(event: @event, title:, content: json_content, aasm_state: :template_draft, author: @author, template_type: self.class.name)
end

end
Expand Down
63 changes: 63 additions & 0 deletions app/models/announcement/templates/monthly.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

class Announcement
module Templates
class Monthly
include ApplicationHelper

def initialize(event:, author:)
@event = event
@author = author
end

def title
"#{Date.current.strftime("%B %Y")} Update"
end

def json_content(block)
{
type: "doc",
content: [
{ type: "paragraph", content: [{ type: "text", text: "Hey all!" }] },
{
type: "paragraph",
content: [
{
type: "text",
text: "Thank you for your support and generosity! With this funding, we'll be able to better work towards our mission.",
},
],
},
{
type: "paragraph",
content: [
{
type: "text",
text: "We'd like to thank all of the donors from the past month that contributed towards our organization:",
},
],
},
{ type: "donationSummary", attrs: { id: block.id } },
{
type: "paragraph",
content: [
{ type: "text", text: "Best," },
{ type: "hardBreak" },
{ type: "text", text: "The #{@event.name} team" },
],
},
],
}
end

def create
announcement = Announcement.create!(event: @event, title:, content: {}, aasm_state: :template_draft, author: @author, template_type: self.class.name)
block = Announcement::Block::DonationSummary.create!(announcement:, parameters: { start_date: Date.current.beginning_of_month })
announcement.update!(content: json_content(block))
end

end

end

end
2 changes: 1 addition & 1 deletion app/models/announcement/templates/new_donation_tier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def json_content
end

def create
Announcement.create!(event: @donation_tier.event, title:, content: json_content, aasm_state: :template_draft, author: @author)
Announcement.create!(event: @donation_tier.event, title:, content: json_content, aasm_state: :template_draft, author: @author, template_type: self.class.name)
end

end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def json_content
end

def create
Announcement.create!(event: @event, title:, content: json_content, aasm_state: :template_draft, author: @author)
Announcement.create!(event: @event, title:, content: json_content, aasm_state: :template_draft, author: @author, template_type: self.class.name)
end

end
Expand Down
2 changes: 1 addition & 1 deletion app/models/announcement/templates/new_team_member.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def json_content
end

def create
Announcement.create!(event: @invite.event, title:, content: json_content, aasm_state: :template_draft, author: @author)
Announcement.create!(event: @invite.event, title:, content: json_content, aasm_state: :template_draft, author: @author, template_type: self.class.name)
end

end
Expand Down
8 changes: 8 additions & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ class Event < ApplicationRecord
after_validation :move_friendly_id_error_to_slug

after_update :generate_stripe_card_designs, if: -> { attachment_changes["stripe_card_logo"].present? && stripe_card_logo.attached? && !Rails.env.test? }
before_save :enable_monthly_announcements

comma do
id
Expand Down Expand Up @@ -834,4 +835,11 @@ def enforce_transparency_eligibility
end
end

def enable_monthly_announcements
# We'll enable monthly announcements when transparency mode is turned on
if is_public_changed?(to: true)
config.update(generate_monthly_announcement: true)
end
end

end
23 changes: 16 additions & 7 deletions app/models/event/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
#
# Table name: event_configurations
#
# id :bigint not null, primary key
# anonymous_donations :boolean default(FALSE)
# contact_email :string
# cover_donation_fees :boolean default(FALSE)
# created_at :datetime not null
# updated_at :datetime not null
# event_id :bigint not null
# id :bigint not null, primary key
# anonymous_donations :boolean default(FALSE)
# contact_email :string
# cover_donation_fees :boolean default(FALSE)
# generate_monthly_announcement :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# event_id :bigint not null
#
# Indexes
#
Expand All @@ -26,6 +27,14 @@ class Configuration < ApplicationRecord
validates_email_format_of :contact_email, allow_nil: true, allow_blank: true
normalizes :contact_email, with: ->(contact_email) { contact_email.strip.downcase }

after_create :set_defaults

private

def set_defaults
self.generate_monthly_announcement = event.is_public unless self.generate_monthly_announcement.present?
end

end

end
3 changes: 3 additions & 0 deletions app/views/announcement_mailer/_announcement.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1><%= @announcement.title %></h1>

<%= @announcement.render_email.html_safe %> <%# erb_lint:disable ErbSafety %>
4 changes: 4 additions & 0 deletions app/views/announcement_mailer/_explanation.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>
Announcements are a feature where you can send out updates to users following your organization.
You are receiving this message because you have auto-scheduled monthly announcements enabled in your organization settings.
</p>
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<h1><%= @announcement.title %></h1>
<%= render "announcement_mailer/announcement" %>

<%= @announcement.render_email.html_safe %> <%# erb_lint:disable ErbSafety %>
<br>
<i><%= link_to "View this announcement in your browser", @announcement %></i>
<br>
Expand Down
19 changes: 19 additions & 0 deletions app/views/announcement_mailer/seven_day_warning.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h1>A scheduled announcement will go out in 7 days</h1>

<%= render "announcement_mailer/explanation" %>

<i><%= link_to "Update the announcement and share what your organization has been working on here", @announcement %></i>

<br>
<br>

<div class="card shadow-none border">
<%= render "announcement_mailer/announcement" %>
</div>

<br>

<p>
From,<br>
The HCB Team
</p>
19 changes: 19 additions & 0 deletions app/views/announcement_mailer/two_day_warning.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h1>A scheduled announcement will go out in 2 days</h1>

<%= render "announcement_mailer/explanation" %>

<i><%= link_to "Update the announcement and share what your organization has been working on here", @announcement %></i>

<br>
<br>

<div class="card shadow-none border">
<%= render "announcement_mailer/announcement" %>
</div>

<br>

<p>
From,<br>
The HCB Team
</p>
19 changes: 19 additions & 0 deletions app/views/events/settings/_details.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,25 @@
<%= form.submit "Update", disabled: %>
</div>

<h3 class="mb1" id="announcements_heading">Announcements</h3>
<div class="card mb3" data-controller="accordion">
<div class="field field--checkbox">
<span style="font-weight: 700">Generate monthly announcements</span>
<div class="field--checkbox--switch ml-auto">
<%= form.fields_for :config do |config| %>
<%= config.label :generate_monthly_announcement do %>
<%= config.check_box :generate_monthly_announcement, data: { action: "change->accordion#toggle", target: "accordion.checkbox" }, disabled:, switch: true %>
<span class="slider"></span>
<% end %>
<% end %>
</div>
</div>
<p class="h5 muted mt0 mb1">
Use this to enable auto-scheduled monthly announcements.
</p>
<%= form.submit "Update", disabled: %>
</div>

<h3 class="mb1" id="cards_heading">Contact information</h3>
<div class="card mb3">
<div class="field">
Expand Down
12 changes: 12 additions & 0 deletions config/schedule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,18 @@ monthly_follower_summary_job:
cron: "0 0 1 * *" # run first day of each month
class: "MonthlyFollowerSummaryJob"

announcement_monthly_job:
cron: "0 0 1 * *" # run first day of each month
class: "Announcement::MonthlyJob"

announcement_seven_day_warning_job:
cron: "0 12 -7 * *" # run at 8am ET; 7 days before the end of the month
class: "Announcement::SevenDayWarningJob"

announcement_two_day_warning_job:
cron: "0 12 -2 * *" # run at 8am ET; 2 days before the end of the month
class: "Announcement::TwoDayWarningJob"

delete_old_template_drafts_job:
cron: "0 0 1 * *" # run first day of each month
class: "DeleteOldTemplateDraftsJob"
5 changes: 5 additions & 0 deletions db/migrate/20250716200152_add_template_to_announcements.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddTemplateToAnnouncements < ActiveRecord::Migration[7.2]
def change
add_column :announcements, :template_type, :string
end
end
Loading