mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 11:47:56 +01:00
Send recap emails for new feedback (#440)
This commit is contained in:
committed by
GitHub
parent
ace50e1089
commit
c0d70186f6
3
Gemfile
3
Gemfile
@@ -64,6 +64,9 @@ gem 'rack-cors', '2.0.2'
|
|||||||
# ActiveJob backend
|
# ActiveJob backend
|
||||||
gem 'sidekiq', '7.3.5'
|
gem 'sidekiq', '7.3.5'
|
||||||
|
|
||||||
|
# Cron jobs with sidekiq
|
||||||
|
gem 'sidekiq-cron', '2.0.1'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
||||||
|
|
||||||
|
|||||||
16
Gemfile.lock
16
Gemfile.lock
@@ -85,6 +85,9 @@ GEM
|
|||||||
concurrent-ruby (1.3.4)
|
concurrent-ruby (1.3.4)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
|
cronex (0.15.0)
|
||||||
|
tzinfo
|
||||||
|
unicode (>= 0.4.4.5)
|
||||||
cssbundling-rails (1.1.2)
|
cssbundling-rails (1.1.2)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
date (3.4.0)
|
date (3.4.0)
|
||||||
@@ -96,6 +99,8 @@ GEM
|
|||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
diff-lcs (1.5.1)
|
diff-lcs (1.5.1)
|
||||||
erubi (1.13.0)
|
erubi (1.13.0)
|
||||||
|
et-orbi (1.2.11)
|
||||||
|
tzinfo
|
||||||
execjs (2.10.0)
|
execjs (2.10.0)
|
||||||
factory_bot (5.0.2)
|
factory_bot (5.0.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
@@ -105,6 +110,9 @@ GEM
|
|||||||
ffi (1.17.0)
|
ffi (1.17.0)
|
||||||
friendly_id (5.5.1)
|
friendly_id (5.5.1)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
|
fugit (1.11.1)
|
||||||
|
et-orbi (~> 1, >= 1.2.11)
|
||||||
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
httparty (0.21.0)
|
httparty (0.21.0)
|
||||||
@@ -173,6 +181,7 @@ GEM
|
|||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (2.2.10)
|
rack (2.2.10)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
@@ -264,6 +273,11 @@ GEM
|
|||||||
logger
|
logger
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.22.2)
|
redis-client (>= 0.22.2)
|
||||||
|
sidekiq-cron (2.0.1)
|
||||||
|
cronex (>= 0.13.0)
|
||||||
|
fugit (~> 1.8, >= 1.11.1)
|
||||||
|
globalid (>= 1.0.1)
|
||||||
|
sidekiq (>= 6.5.0)
|
||||||
spring (2.1.1)
|
spring (2.1.1)
|
||||||
spring-watcher-listen (2.0.1)
|
spring-watcher-listen (2.0.1)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
@@ -284,6 +298,7 @@ GEM
|
|||||||
turbolinks-source (5.2.0)
|
turbolinks-source (5.2.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
unicode (0.4.4.5)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.1)
|
web-console (4.2.1)
|
||||||
@@ -330,6 +345,7 @@ DEPENDENCIES
|
|||||||
rswag-specs (= 2.15.0)
|
rswag-specs (= 2.15.0)
|
||||||
selenium-webdriver (= 4.17.0)
|
selenium-webdriver (= 4.17.0)
|
||||||
sidekiq (= 7.3.5)
|
sidekiq (= 7.3.5)
|
||||||
|
sidekiq-cron (= 2.0.1)
|
||||||
spring (= 2.1.1)
|
spring (= 2.1.1)
|
||||||
spring-watcher-listen (= 2.0.1)
|
spring-watcher-listen (= 2.0.1)
|
||||||
stripe (= 11.2.0)
|
stripe (= 11.2.0)
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
label[for=user_notifications_enabled] {
|
||||||
|
@extend .mb-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.apiKeyGenerateButton { width: 100%; }
|
.apiKeyGenerateButton { width: 100%; }
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ class ApplicationController < ActionController::Base
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def configure_devise_permitted_parameters
|
def configure_devise_permitted_parameters
|
||||||
additional_permitted_parameters = [:full_name, :notifications_enabled, :invitation_token]
|
additional_permitted_parameters = [
|
||||||
|
:full_name,
|
||||||
|
:notifications_enabled,
|
||||||
|
:recap_notification_frequency,
|
||||||
|
:invitation_token
|
||||||
|
]
|
||||||
|
|
||||||
devise_parameter_sanitizer.permit(:sign_up, keys: additional_permitted_parameters)
|
devise_parameter_sanitizer.permit(:sign_up, keys: additional_permitted_parameters)
|
||||||
devise_parameter_sanitizer.permit(:account_update, keys: additional_permitted_parameters)
|
devise_parameter_sanitizer.permit(:account_update, keys: additional_permitted_parameters)
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class TenantsController < ApplicationController
|
|||||||
email: params[:user][:email],
|
email: params[:user][:email],
|
||||||
password: is_o_auth_login ? Devise.friendly_token : params[:user][:password],
|
password: is_o_auth_login ? Devise.friendly_token : params[:user][:password],
|
||||||
has_set_password: !is_o_auth_login,
|
has_set_password: !is_o_auth_login,
|
||||||
role: "owner"
|
role: "owner",
|
||||||
|
recap_notification_frequency: "daily"
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_o_auth_login
|
if is_o_auth_login
|
||||||
|
|||||||
87
app/jobs/send_recap_emails.rb
Normal file
87
app/jobs/send_recap_emails.rb
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
class SendRecapEmails < ActiveJob::Base
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(*args)
|
||||||
|
# Fix times to 15:00 UTC
|
||||||
|
time_now = Time.now.utc.change(hour: args[0], min: 0, sec: 0)
|
||||||
|
one_day_ago = 1.day.ago.utc.change(hour: args[0], min: 0, sec: 0)
|
||||||
|
one_week_ago = 1.week.ago.utc.change(hour: args[0], min: 0, sec: 0)
|
||||||
|
one_month_ago = 1.month.ago.utc.change(hour: args[0], min: 0, sec: 0)
|
||||||
|
|
||||||
|
# Get tenants with active subscriptions
|
||||||
|
tbs = TenantBilling.unscoped.all
|
||||||
|
tbs = tbs.select { |tb| tb.has_active_subscription? }
|
||||||
|
tenants = Tenant.where(id: tbs.map(&:tenant_id))
|
||||||
|
|
||||||
|
# Based on the current date, determine which recap notifications to send
|
||||||
|
frequencies_to_notify = ['daily']
|
||||||
|
frequencies_to_notify.push('weekly') if Date.today.monday? # Send weekly recap on Mondays
|
||||||
|
frequencies_to_notify.push('monthly') if Date.today.day == 1 # Send monthly recap on the 1st of the month
|
||||||
|
|
||||||
|
tenants.each do |tenant|
|
||||||
|
Current.tenant = tenant
|
||||||
|
I18n.locale = tenant.locale
|
||||||
|
|
||||||
|
# Get users with recap notifications enabled
|
||||||
|
users = tenant.users.where(
|
||||||
|
role: ['owner', 'admin', 'moderator'],
|
||||||
|
notifications_enabled: true,
|
||||||
|
recap_notification_frequency: frequencies_to_notify,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the different recap notification frequencies for users
|
||||||
|
users_recap_notification_frequencies = users.map(&:recap_notification_frequency).flatten.uniq
|
||||||
|
|
||||||
|
# Get only needed posts
|
||||||
|
if users_recap_notification_frequencies.include?('daily')
|
||||||
|
published_posts_daily = Post.where(approval_status: 'approved', created_at: one_day_ago..time_now).to_a
|
||||||
|
pending_posts_daily = Post.where(approval_status: 'pending', created_at: one_day_ago..time_now).to_a
|
||||||
|
end
|
||||||
|
if frequencies_to_notify.include?('weekly') && users_recap_notification_frequencies.include?('weekly')
|
||||||
|
published_posts_weekly = Post.where(approval_status: 'approved', created_at: one_week_ago..time_now).to_a
|
||||||
|
pending_posts_weekly = Post.where(approval_status: 'pending', created_at: one_week_ago..time_now).to_a
|
||||||
|
end
|
||||||
|
if frequencies_to_notify.include?('monthly') && users_recap_notification_frequencies.include?('monthly')
|
||||||
|
published_posts_monthly = Post.where(approval_status: 'approved', created_at: one_month_ago..time_now).to_a
|
||||||
|
pending_posts_monthly = Post.where(approval_status: 'pending', created_at: one_month_ago..time_now).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
# Notify each user based on their recap notification frequency
|
||||||
|
users.each do |user|
|
||||||
|
# Remove from published_posts the posts published by the user
|
||||||
|
published_posts_daily_user = published_posts_daily&.select { |post| post.user_id != user.id }
|
||||||
|
should_send_daily_recap = published_posts_daily_user&.any? || pending_posts_daily&.any?
|
||||||
|
|
||||||
|
published_posts_weekly_user = published_posts_weekly&.select { |post| post.user_id != user.id }
|
||||||
|
should_send_weekly_recap = published_posts_weekly_user&.any? || pending_posts_weekly&.any?
|
||||||
|
|
||||||
|
published_posts_monthly_user = published_posts_monthly&.select { |post| post.user_id != user.id }
|
||||||
|
should_send_monthly_recap = published_posts_monthly_user&.any? || pending_posts_monthly&.any?
|
||||||
|
|
||||||
|
# Send recap email
|
||||||
|
if user.recap_notification_frequency == 'daily' && should_send_daily_recap
|
||||||
|
UserMailer.recap(
|
||||||
|
frequency: I18n.t('common.forms.auth.recap_notification_frequency_daily'),
|
||||||
|
user: user,
|
||||||
|
published_posts_count: published_posts_daily_user&.count,
|
||||||
|
pending_posts_count: pending_posts_daily&.count,
|
||||||
|
).deliver_later
|
||||||
|
elsif user.recap_notification_frequency == 'weekly' && should_send_weekly_recap
|
||||||
|
UserMailer.recap(
|
||||||
|
frequency: I18n.t('common.forms.auth.recap_notification_frequency_weekly'),
|
||||||
|
user: user,
|
||||||
|
published_posts_count: published_posts_weekly_user&.count,
|
||||||
|
pending_posts_count: pending_posts_weekly&.count,
|
||||||
|
).deliver_later
|
||||||
|
elsif user.recap_notification_frequency == 'monthly' && should_send_monthly_recap
|
||||||
|
UserMailer.recap(
|
||||||
|
frequency: I18n.t('common.forms.auth.recap_notification_frequency_monthly'),
|
||||||
|
user: user,
|
||||||
|
published_posts_count: published_posts_monthly_user&.count,
|
||||||
|
pending_posts_count: pending_posts_monthly&.count,
|
||||||
|
).deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -43,6 +43,21 @@ class UserMailer < ApplicationMailer
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recap(frequency:, user:, published_posts_count:, pending_posts_count:)
|
||||||
|
Current.tenant = user.tenant
|
||||||
|
|
||||||
|
@frequency = frequency
|
||||||
|
@user = user
|
||||||
|
@published_posts_count = published_posts_count
|
||||||
|
@pending_posts_count = pending_posts_count
|
||||||
|
|
||||||
|
mail(
|
||||||
|
to: user.email,
|
||||||
|
subject: t('mailers.user.recap.subject', site_name: site_name, frequency: frequency)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def site_name
|
def site_name
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class User < ApplicationRecord
|
|||||||
enum role: [:user, :moderator, :admin, :owner]
|
enum role: [:user, :moderator, :admin, :owner]
|
||||||
enum status: [:active, :blocked, :deleted]
|
enum status: [:active, :blocked, :deleted]
|
||||||
|
|
||||||
|
enum recap_notification_frequency: [:never, :daily, :weekly, :monthly]
|
||||||
|
|
||||||
after_initialize :set_default_role, if: :new_record?
|
after_initialize :set_default_role, if: :new_record?
|
||||||
after_initialize :set_default_status, if: :new_record?
|
after_initialize :set_default_status, if: :new_record?
|
||||||
|
|
||||||
|
|||||||
@@ -34,15 +34,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<%= f.label :notifications_enabled, t('common.forms.auth.notifications_enabled') %>
|
|
||||||
|
|
||||||
<%= f.check_box :notifications_enabled, style: "transform: scale(1.5)" %>
|
|
||||||
<small id="notificationsHelp" class="form-text text-muted">
|
|
||||||
<%= t('common.forms.auth.notifications_enabled_help') %>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :password, t('common.forms.auth.password') %>
|
<%= f.label :password, t('common.forms.auth.password') %>
|
||||||
<%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
|
<%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
|
||||||
@@ -58,6 +49,39 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
<h3><%= t('common.forms.auth.notifications') %></h3>
|
||||||
|
<br />
|
||||||
|
<div class="form-group">
|
||||||
|
<%= f.label :notifications_enabled, t('activerecord.attributes.user.notifications_enabled') %>
|
||||||
|
|
||||||
|
<%= f.check_box :notifications_enabled, style: "transform: scale(1.5)" %>
|
||||||
|
<small id="notificationsHelp" class="form-text text-muted">
|
||||||
|
<%= t('common.forms.auth.notifications_enabled_help') %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if Rails.application.sidekiq_enabled? %>
|
||||||
|
<div class="form-group">
|
||||||
|
<%= f.label :recap_notification_frequency, t('activerecord.attributes.user.recap_notification_frequency') %>
|
||||||
|
<%= f.select :recap_notification_frequency,
|
||||||
|
[
|
||||||
|
[t('common.forms.auth.recap_notification_frequency_never'), "never"],
|
||||||
|
[t('common.forms.auth.recap_notification_frequency_daily'), "daily"],
|
||||||
|
[t('common.forms.auth.recap_notification_frequency_weekly'), "weekly"],
|
||||||
|
[t('common.forms.auth.recap_notification_frequency_monthly'), "monthly"]
|
||||||
|
],
|
||||||
|
{ include_blank: false },
|
||||||
|
class: "form-control" %>
|
||||||
|
<small id="recapNotificationFrequencyHelp" class="form-text text-muted">
|
||||||
|
<%= t('common.forms.auth.recap_notification_frequency_help') %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p>You have to <a href="https://docs.astuto.io/deploy-with-sidekiq">enable Sidekiq</a> to receive recap notifications.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :current_password, t('common.forms.auth.current_password') %>
|
<%= f.label :current_password, t('common.forms.auth.current_password') %>
|
||||||
<%= f.password_field :current_password, autocomplete: "current-password", required: true, class: "form-control" %>
|
<%= f.password_field :current_password, autocomplete: "current-password", required: true, class: "form-control" %>
|
||||||
|
|||||||
18
app/views/user_mailer/recap.html.erb
Normal file
18
app/views/user_mailer/recap.html.erb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<%= render 'user_mailer/opening', user_name: @user.full_name_or_email %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= t('mailers.user.recap.body_html', frequency: @frequency.downcase) %>:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><%= t('mailers.user.recap.published_posts_count_html', count: @published_posts_count) %></li>
|
||||||
|
<li><%= t('mailers.user.recap.pending_posts_count_html', count: @pending_posts_count) %></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= link_to t('mailers.user.learn_more'), get_url_for(method(:root_url)) %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<%= render 'user_mailer/closing' %>
|
||||||
|
|
||||||
|
<%= render 'user_mailer/unsubscribe_from_site' %>
|
||||||
@@ -67,5 +67,9 @@ module App
|
|||||||
def stripe_yearly_lookup_key
|
def stripe_yearly_lookup_key
|
||||||
ENV["STRIPE_YEARLY_LOOKUP_KEY"]
|
ENV["STRIPE_YEARLY_LOOKUP_KEY"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sidekiq_enabled?
|
||||||
|
ENV["ACTIVE_JOB_BACKEND"] == "sidekiq"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
12
config/initializers/sidekiq_cron.rb
Normal file
12
config/initializers/sidekiq_cron.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Sidekiq::Cron.configure do |config|
|
||||||
|
config.cron_schedule_file = 'config/sidekiq_cron_schedule.yml'
|
||||||
|
|
||||||
|
config.cron_poll_interval = 30
|
||||||
|
config.cron_history_size = 50
|
||||||
|
config.default_namespace = 'default'
|
||||||
|
config.natural_cron_parsing_mode = :strict
|
||||||
|
|
||||||
|
# Handles the case when the Sidekiq process was down for a while and the cron job should have run (set to 10 minutes, i.e. 600 seconds)
|
||||||
|
# This could happen during the deployment of a new version of the application
|
||||||
|
config.reschedule_grace_period = 600
|
||||||
|
end
|
||||||
@@ -50,6 +50,11 @@ en:
|
|||||||
notify_follower_of_post_status_change:
|
notify_follower_of_post_status_change:
|
||||||
subject: '[%{site_name}] Status change on post "%{post}"'
|
subject: '[%{site_name}] Status change on post "%{post}"'
|
||||||
body_html: 'There is a status update on the post you are following <b>%{post}</b>'
|
body_html: 'There is a status update on the post you are following <b>%{post}</b>'
|
||||||
|
recap:
|
||||||
|
subject: '[%{site_name}] %{frequency} recap of feedback space activity'
|
||||||
|
body_html: 'Here is the %{frequency} recap of activities in your feedback space'
|
||||||
|
published_posts_count_html: 'New published feedback: %{count}'
|
||||||
|
pending_posts_count_html: 'New feedback pending approval: %{count}'
|
||||||
activerecord:
|
activerecord:
|
||||||
models:
|
models:
|
||||||
board:
|
board:
|
||||||
@@ -148,6 +153,7 @@ en:
|
|||||||
password_confirmation: 'Password confirmation'
|
password_confirmation: 'Password confirmation'
|
||||||
role: 'Role'
|
role: 'Role'
|
||||||
notifications_enabled: 'Notifications enabled'
|
notifications_enabled: 'Notifications enabled'
|
||||||
|
recap_notification_frequency: 'Recap notification frequency'
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
invalid: 'is invalid'
|
invalid: 'is invalid'
|
||||||
|
|||||||
@@ -19,8 +19,13 @@ en:
|
|||||||
new_password: 'New password'
|
new_password: 'New password'
|
||||||
new_password_confirmation: 'New password confirmation'
|
new_password_confirmation: 'New password confirmation'
|
||||||
current_password: 'Current password'
|
current_password: 'Current password'
|
||||||
notifications_enabled: 'Notifications enabled'
|
notifications: 'Notifications'
|
||||||
notifications_enabled_help: "if disabled, you won't receive any notification"
|
notifications_enabled_help: "if disabled, you won't receive any notification"
|
||||||
|
recap_notification_frequency_never: 'Never'
|
||||||
|
recap_notification_frequency_daily: 'Daily'
|
||||||
|
recap_notification_frequency_weekly: 'Weekly'
|
||||||
|
recap_notification_frequency_monthly: 'Monthly'
|
||||||
|
recap_notification_frequency_help: 'recap notifications let you know if new feedback has been submitted or is waiting for your approval'
|
||||||
waiting_confirmation: 'Currently waiting confirmation for %{email}'
|
waiting_confirmation: 'Currently waiting confirmation for %{email}'
|
||||||
no_password_set: 'You must set a password to update your profile'
|
no_password_set: 'You must set a password to update your profile'
|
||||||
set_password: 'Set password'
|
set_password: 'Set password'
|
||||||
|
|||||||
9
config/sidekiq_cron_schedule.yml
Normal file
9
config/sidekiq_cron_schedule.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# For crontab syntax, see https://crontab.guru/
|
||||||
|
|
||||||
|
send_recap_emails:
|
||||||
|
cron: "0 15 * * *" # At 15:00 every day
|
||||||
|
# cron: "*/30 * * * * *" # Execute every 30 seconds (for testing purposes)
|
||||||
|
class: "SendRecapEmails"
|
||||||
|
queue: default
|
||||||
|
args:
|
||||||
|
hour: 15 # This should be in sync with the "cron" time
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddRecapNotificationFrequencyToUsers < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :users, :recap_notification_frequency, :integer, default: 0, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2024_10_04_170520) do
|
ActiveRecord::Schema.define(version: 2024_11_18_082824) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -239,6 +239,7 @@ ActiveRecord::Schema.define(version: 2024_10_04_170520) do
|
|||||||
t.datetime "last_sign_in_at"
|
t.datetime "last_sign_in_at"
|
||||||
t.string "current_sign_in_ip"
|
t.string "current_sign_in_ip"
|
||||||
t.string "last_sign_in_ip"
|
t.string "last_sign_in_ip"
|
||||||
|
t.integer "recap_notification_frequency", default: 0, null: false
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["email", "tenant_id"], name: "index_users_on_email_and_tenant_id", unique: true
|
t.index ["email", "tenant_id"], name: "index_users_on_email_and_tenant_id", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
|
|||||||
187
spec/jobs/send_recap_emails_spec.rb
Normal file
187
spec/jobs/send_recap_emails_spec.rb
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
include ActiveSupport::Testing::TimeHelpers
|
||||||
|
|
||||||
|
RSpec.describe SendRecapEmails, type: :job do
|
||||||
|
before do
|
||||||
|
@hour_of_execution = 15
|
||||||
|
|
||||||
|
@admin = FactoryBot.create(:user, role: 'admin', notifications_enabled: true)
|
||||||
|
allow(UserMailer).to receive(:recap).and_call_original
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a daily recap email with published posts count and pending posts count' do
|
||||||
|
@admin.recap_notification_frequency = 'daily'
|
||||||
|
@admin.save
|
||||||
|
|
||||||
|
travel_to Time.now.utc.change(hour: @hour_of_execution) do
|
||||||
|
# Published posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 23.hours.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 16.hours.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 30.hours.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
# Pending posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 23.hours.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 30.hours.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
expect(UserMailer).to have_received(:recap).with(
|
||||||
|
frequency: 'Daily',
|
||||||
|
user: @admin,
|
||||||
|
published_posts_count: 2,
|
||||||
|
pending_posts_count: 1
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not count posts published by the user receiving the recap email' do
|
||||||
|
@admin.recap_notification_frequency = 'daily'
|
||||||
|
@admin.save
|
||||||
|
|
||||||
|
travel_to Time.now.utc.change(hour: @hour_of_execution) do
|
||||||
|
# Published posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 23.hours.ago, user: @admin) # Should not be included in recap
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 16.hours.ago)
|
||||||
|
|
||||||
|
# Pending posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 23.hours.ago)
|
||||||
|
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
expect(UserMailer).to have_received(:recap).with(
|
||||||
|
frequency: 'Daily',
|
||||||
|
user: @admin,
|
||||||
|
published_posts_count: 1,
|
||||||
|
pending_posts_count: 1
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a recap email for every owner/admin/mod with notifications enabled and recap_notification_frequency set' do
|
||||||
|
@admin.recap_notification_frequency = 'daily'
|
||||||
|
@admin.save
|
||||||
|
owner = FactoryBot.create(:user, role: 'owner', notifications_enabled: true, recap_notification_frequency: 'daily')
|
||||||
|
mod = FactoryBot.create(:user, role: 'moderator', notifications_enabled: false, recap_notification_frequency: 'daily') # Should not receive recap
|
||||||
|
|
||||||
|
travel_to Time.now.utc.change(hour: @hour_of_execution) do
|
||||||
|
# Published posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 23.hours.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 16.hours.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 30.hours.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
# Pending posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 23.hours.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 30.hours.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
expect(UserMailer).to have_received(:recap).with(
|
||||||
|
frequency: 'Daily',
|
||||||
|
user: @admin,
|
||||||
|
published_posts_count: 2,
|
||||||
|
pending_posts_count: 1
|
||||||
|
).once
|
||||||
|
expect(UserMailer).to have_received(:recap).with(
|
||||||
|
frequency: 'Daily',
|
||||||
|
user: owner,
|
||||||
|
published_posts_count: 2,
|
||||||
|
pending_posts_count: 1
|
||||||
|
).once
|
||||||
|
expect(UserMailer).not_to have_received(:recap).with(
|
||||||
|
frequency: 'Daily',
|
||||||
|
user: mod,
|
||||||
|
published_posts_count: 2,
|
||||||
|
pending_posts_count: 1
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a weekly recap email with published posts count and pending posts count on Monday' do
|
||||||
|
@admin.recap_notification_frequency = 'weekly'
|
||||||
|
@admin.save
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 18, @hour_of_execution, 0, 0) do # Monday
|
||||||
|
# Published posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 6.days.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 10.days.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
# Pending posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 1.minute.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 6.days.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 10.days.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
expect(UserMailer).to have_received(:recap).with(
|
||||||
|
frequency: 'Weekly',
|
||||||
|
user: @admin,
|
||||||
|
published_posts_count: 2,
|
||||||
|
pending_posts_count: 2
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send a weekly recap email on days other than Monday' do
|
||||||
|
@admin.recap_notification_frequency = 'weekly'
|
||||||
|
@admin.save
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 19, @hour_of_execution, 0, 0) do # Tuesday
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
expect(UserMailer).not_to have_received(:recap)
|
||||||
|
end
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 20, @hour_of_execution, 0, 0) do # Wednesday
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
expect(UserMailer).not_to have_received(:recap)
|
||||||
|
end
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 21, @hour_of_execution, 0, 0) do # Thursday
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
expect(UserMailer).not_to have_received(:recap)
|
||||||
|
end
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 22, @hour_of_execution, 0, 0) do # Friday
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
expect(UserMailer).not_to have_received(:recap)
|
||||||
|
end
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 23, @hour_of_execution, 0, 0) do # Saturday
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
expect(UserMailer).not_to have_received(:recap)
|
||||||
|
end
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 24, @hour_of_execution, 0, 0) do # Sunday
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
expect(UserMailer).not_to have_received(:recap)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a monthly recap email with published posts count and pending posts count on the first day of the month' do
|
||||||
|
@admin.recap_notification_frequency = 'monthly'
|
||||||
|
@admin.save
|
||||||
|
|
||||||
|
travel_to Time.zone.local(2024, 11, 1, @hour_of_execution, 0, 0) do # First day of the month
|
||||||
|
# Published posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 1.hour.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.days.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 3.weeks.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'approved', created_at: 2.months.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
# Pending posts
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 2.days.ago)
|
||||||
|
FactoryBot.create(:post, approval_status: 'pending', created_at: 2.months.ago) # Should not be included in recap
|
||||||
|
|
||||||
|
SendRecapEmails.perform_now(@hour_of_execution)
|
||||||
|
|
||||||
|
expect(UserMailer).to have_received(:recap).with(
|
||||||
|
frequency: 'Monthly',
|
||||||
|
user: @admin,
|
||||||
|
published_posts_count: 3,
|
||||||
|
pending_posts_count: 1
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -33,4 +33,13 @@ class UserMailerPreview < ActionMailer::Preview
|
|||||||
follower = comment.post.follows.first.user
|
follower = comment.post.follows.first.user
|
||||||
UserMailer.notify_follower_of_post_status_change(post: Post.first, follower: follower)
|
UserMailer.notify_follower_of_post_status_change(post: Post.first, follower: follower)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recap
|
||||||
|
UserMailer.recap(
|
||||||
|
frequency: I18n.t('common.forms.auth.recap_notification_frequency_daily'),
|
||||||
|
user: User.first,
|
||||||
|
published_posts_count: 3,
|
||||||
|
pending_posts_count: 2,
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user