Add weekly notification
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
f945dbe14a
commit
d0fbf11e4e
@ -7,7 +7,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Service.Workers.Notification
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.Setting
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
require Logger
|
||||
|
||||
def before_event_notification(%Participant{
|
||||
@ -73,10 +73,75 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
|
||||
|
||||
def on_day_notification(_), do: {:ok, nil}
|
||||
|
||||
def weekly_notification(%Participant{
|
||||
event: %Event{begins_on: begins_on},
|
||||
actor: %Actor{user_id: user_id}
|
||||
})
|
||||
when not is_nil(user_id) do
|
||||
%User{settings: settings, locale: locale} = Users.get_user_with_settings!(user_id)
|
||||
|
||||
case settings do
|
||||
%Setting{notification_each_week: true, timezone: timezone} ->
|
||||
%DateTime{} = begins_on_shifted = shift_zone(begins_on, timezone)
|
||||
|
||||
Logger.debug(
|
||||
"Participation event start at #{inspect(begins_on_shifted)} (user timezone is #{
|
||||
timezone
|
||||
})"
|
||||
)
|
||||
|
||||
notification_date =
|
||||
unless begins_on < DateTime.utc_now() do
|
||||
notification_day = calculate_first_day_of_week(DateTime.to_date(begins_on), locale)
|
||||
|
||||
{:ok, %NaiveDateTime{} = notification_date} =
|
||||
notification_day |> NaiveDateTime.new(~T[08:00:00])
|
||||
|
||||
# This is the datetime when the notification should be sent
|
||||
{:ok, %DateTime{} = notification_date} =
|
||||
DateTime.from_naive(notification_date, timezone)
|
||||
|
||||
unless notification_date < DateTime.utc_now() do
|
||||
notification_date
|
||||
else
|
||||
nil
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
Logger.debug(
|
||||
"Participation notification should be sent at #{inspect(notification_date)} (user timezone)"
|
||||
)
|
||||
|
||||
if is_nil(notification_date) do
|
||||
{:ok, "Too late to send weekly notifications"}
|
||||
else
|
||||
Notification.enqueue(:weekly_notification, %{user_id: user_id},
|
||||
scheduled_at: notification_date
|
||||
)
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:ok, "User has disabled weekly notifications"}
|
||||
end
|
||||
end
|
||||
|
||||
def weekly_notification(_), do: {:ok, nil}
|
||||
|
||||
defp shift_zone(datetime, timezone) do
|
||||
case DateTime.shift_zone(datetime, timezone) do
|
||||
{:ok, shift_datetime} -> shift_datetime
|
||||
{:error, _} -> datetime
|
||||
end
|
||||
end
|
||||
|
||||
defp calculate_first_day_of_week(%Date{} = date, locale) do
|
||||
day_number = Date.day_of_week(date)
|
||||
first_day_number = Cldr.Calendar.first_day_for_locale(locale)
|
||||
|
||||
if day_number == first_day_number,
|
||||
do: date,
|
||||
else: calculate_first_day_of_week(Date.add(date, -1), locale)
|
||||
end
|
||||
end
|
||||
|
@ -28,29 +28,55 @@ defmodule Mobilizon.Service.Workers.Notification do
|
||||
end
|
||||
|
||||
def perform(%{"op" => "on_day_notification", "user_id" => user_id}, _job) do
|
||||
%User{locale: locale, settings: %Setting{timezone: timezone, notification_on_day: true}} =
|
||||
user = Users.get_user_with_settings!(user_id)
|
||||
|
||||
now = DateTime.utc_now()
|
||||
%DateTime{} = now_shifted = shift_zone(now, timezone)
|
||||
start = %{now_shifted | hour: 8, minute: 0, second: 0, microsecond: {0, 0}}
|
||||
tomorrow = DateTime.add(start, 3600 * 24)
|
||||
|
||||
with %Page{
|
||||
with %User{locale: locale, settings: %Setting{timezone: timezone, notification_on_day: true}} =
|
||||
user <- Users.get_user_with_settings!(user_id),
|
||||
{start, tomorrow} <- calculate_start_end(1, timezone),
|
||||
%Page{
|
||||
elements: participations,
|
||||
total: total
|
||||
} <-
|
||||
}
|
||||
when total > 0 <-
|
||||
Events.list_participations_for_user(user_id, start, tomorrow, 1, 5),
|
||||
true <-
|
||||
Enum.all?(participations, fn participation ->
|
||||
participations <-
|
||||
Enum.filter(participations, fn participation ->
|
||||
participation.event.status == :confirmed
|
||||
end),
|
||||
true <- total > 0 do
|
||||
true <- length(participations) > 0 do
|
||||
user
|
||||
|> Notification.on_day_notification(participations, total, locale)
|
||||
|> Mailer.deliver_later()
|
||||
|
||||
:ok
|
||||
else
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def perform(%{"op" => "weekly_notification", "user_id" => user_id}, _job) do
|
||||
with %User{
|
||||
locale: locale,
|
||||
settings: %Setting{timezone: timezone, notification_each_week: true}
|
||||
} = user <- Users.get_user_with_settings!(user_id),
|
||||
{start, end_week} <- calculate_start_end(7, timezone),
|
||||
%Page{
|
||||
elements: participations,
|
||||
total: total
|
||||
}
|
||||
when total > 0 <-
|
||||
Events.list_participations_for_user(user_id, start, end_week, 1, 5),
|
||||
participations <-
|
||||
Enum.filter(participations, fn participation ->
|
||||
participation.event.status == :confirmed
|
||||
end),
|
||||
true <- length(participations) > 0 do
|
||||
user
|
||||
|> Notification.weekly_notification(participations, total, locale)
|
||||
|> Mailer.deliver_later()
|
||||
|
||||
:ok
|
||||
else
|
||||
_err ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@ -60,4 +86,16 @@ defmodule Mobilizon.Service.Workers.Notification do
|
||||
{:error, _} -> datetime
|
||||
end
|
||||
end
|
||||
|
||||
defp calculate_start_end(days, timezone) do
|
||||
now = DateTime.utc_now()
|
||||
%DateTime{} = now_shifted = shift_zone(now, timezone)
|
||||
start = %{now_shifted | hour: 8, minute: 0, second: 0, microsecond: {0, 0}}
|
||||
|
||||
{:ok, %NaiveDateTime{} = tomorrow} =
|
||||
Date.utc_today() |> Date.add(days) |> NaiveDateTime.new(~T[08:00:00])
|
||||
|
||||
{:ok, %DateTime{} = tomorrow} = DateTime.from_naive(tomorrow, timezone)
|
||||
{start, tomorrow}
|
||||
end
|
||||
end
|
||||
|
@ -56,4 +56,28 @@ defmodule Mobilizon.Web.Email.Notification do
|
||||
|> assign(:subject, subject)
|
||||
|> render(:on_day_notification)
|
||||
end
|
||||
|
||||
def weekly_notification(
|
||||
%User{email: email, settings: %Setting{timezone: timezone}},
|
||||
participations,
|
||||
total,
|
||||
locale \\ "en"
|
||||
) do
|
||||
Gettext.put_locale(locale)
|
||||
participation = hd(participations)
|
||||
|
||||
subject =
|
||||
ngettext("One event planned this week", "%{nb_events} events planned this week", total,
|
||||
nb_events: total
|
||||
)
|
||||
|
||||
Email.base_email(to: email, subject: subject)
|
||||
|> assign(:locale, locale)
|
||||
|> assign(:participation, participation)
|
||||
|> assign(:participations, participations)
|
||||
|> assign(:total, total)
|
||||
|> assign(:timezone, timezone)
|
||||
|> assign(:subject, subject)
|
||||
|> render(:notification_each_week)
|
||||
end
|
||||
end
|
||||
|
@ -42,10 +42,6 @@
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #f4f4f4; margin: 0 !important; padding: 0 !important;">
|
||||
<!-- HIDDEN PREHEADER TEXT -->
|
||||
<!--<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Lato', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
|
||||
Looks like you tried signing in a few too many times. Let's see if we can get you back into your account.
|
||||
</div>-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<!-- LOGO -->
|
||||
<tr>
|
||||
|
81
lib/web/templates/email/notification_each_week.html.eex
Normal file
81
lib/web/templates/email/notification_each_week.html.eex
Normal file
@ -0,0 +1,81 @@
|
||||
<!-- HERO -->
|
||||
<tr>
|
||||
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "Events this week" %>
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- COPY BLOCK -->
|
||||
<tr>
|
||||
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<!-- COPY -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= ngettext "You have one event this week:", "You have %{total} events this week:", @total, total: @total %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<%= if @total > 1 do %>
|
||||
<ul style="margin: 0;">
|
||||
<%= for participation <- @participations do %>
|
||||
<li>
|
||||
<strong>
|
||||
<%= participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_string(@locale) %>
|
||||
</strong>
|
||||
<a href="<%= page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %>" target="_blank">
|
||||
<%= participation.event.title %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<strong>
|
||||
<%= @participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_string(@locale) %>
|
||||
</strong>
|
||||
<a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %>" target="_blank">
|
||||
<%= @participation.event.title %>
|
||||
</a>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
|
||||
<p style="margin: 0">
|
||||
<%= ngettext "If you need to cancel your participation, just access the event page through the link above and click on the participation button.", "If you need to cancel your participation, just access the event page through the links above and click on the participation button.", @total %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
14
lib/web/templates/email/notification_each_week.text.eex
Normal file
14
lib/web/templates/email/notification_each_week.text.eex
Normal file
@ -0,0 +1,14 @@
|
||||
<%= gettext "Events this week" %>
|
||||
==
|
||||
|
||||
<%= ngettext "You have one event this week:", "You have %{total} events this week:", @total, total: @total %>
|
||||
|
||||
<%= if @total > 1 do %>
|
||||
<%= for participation <- @participations do %>
|
||||
- <%= participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_string(@locale) %> - <%= participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= DateTime.shift_zone!(@participation.event.begins_on, @timezone) |> datetime_to_string(@locale) %> - <%= @participation.event.title %> <%= page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %>
|
||||
<% end %>
|
||||
|
||||
<%= ngettext "If you need to cancel your participation, just access the event page through the link above and click on the participation button.", "If you need to cancel your participation, just access the event page through the links above and click on the participation button.", @total %>
|
@ -144,4 +144,92 @@ defmodule Mobilizon.Service.Notifications.SchedulerTest do
|
||||
Scheduler.on_day_notification(participant)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Joining an event registers a job for notification on week of the event" do
|
||||
test "if the user has allowed it" do
|
||||
%User{id: user_id} = user = insert(:user, locale: "fr")
|
||||
|
||||
settings =
|
||||
insert(:settings, user_id: user_id, notification_each_week: true, timezone: "Europe/Paris")
|
||||
|
||||
user = Map.put(user, :settings, settings)
|
||||
actor = insert(:actor, user: user)
|
||||
|
||||
# Make sure event happens next week
|
||||
%Date{} = event_day = Date.utc_today() |> Date.add(7)
|
||||
{:ok, %NaiveDateTime{} = event_date} = event_day |> NaiveDateTime.new(~T[16:00:00])
|
||||
{:ok, begins_on} = DateTime.from_naive(event_date, "Etc/UTC")
|
||||
|
||||
%Event{} = event = insert(:event, begins_on: begins_on)
|
||||
|
||||
%Participant{} = participant = insert(:participant, actor: actor, event: event)
|
||||
|
||||
Scheduler.weekly_notification(participant)
|
||||
|
||||
{:ok, scheduled_at} =
|
||||
begins_on
|
||||
|> DateTime.to_date()
|
||||
|> calculate_first_day_of_week("fr")
|
||||
|> NaiveDateTime.new(~T[08:00:00])
|
||||
|
||||
{:ok, scheduled_at} = DateTime.from_naive(scheduled_at, "Europe/Paris")
|
||||
|
||||
assert_enqueued(
|
||||
worker: Notification,
|
||||
args: %{user_id: user_id, op: :weekly_notification},
|
||||
scheduled_at: scheduled_at
|
||||
)
|
||||
end
|
||||
|
||||
test "not if the user hasn't allowed it" do
|
||||
%User{id: user_id} = user = insert(:user)
|
||||
actor = insert(:actor, user: user)
|
||||
|
||||
%Participant{} = participant = insert(:participant, actor: actor)
|
||||
|
||||
Scheduler.weekly_notification(participant)
|
||||
|
||||
refute_enqueued(
|
||||
worker: Notification,
|
||||
args: %{user_id: user_id, op: :weekly_notification}
|
||||
)
|
||||
end
|
||||
|
||||
test "not if it's too late" do
|
||||
%User{id: user_id} = user = insert(:user)
|
||||
|
||||
settings =
|
||||
insert(:settings, user_id: user_id, notification_on_day: true, timezone: "Europe/Paris")
|
||||
|
||||
user = Map.put(user, :settings, settings)
|
||||
actor = insert(:actor, user: user)
|
||||
|
||||
{:ok, begins_on} =
|
||||
Date.utc_today()
|
||||
|> calculate_first_day_of_week("fr")
|
||||
|> NaiveDateTime.new(~T[05:00:00])
|
||||
|
||||
{:ok, begins_on} = DateTime.from_naive(begins_on, "Europe/Paris")
|
||||
|
||||
%Event{} = event = insert(:event, begins_on: begins_on)
|
||||
|
||||
%Participant{} = participant = insert(:participant, actor: actor, event: event)
|
||||
|
||||
Scheduler.weekly_notification(participant)
|
||||
|
||||
refute_enqueued(
|
||||
worker: Notification,
|
||||
args: %{user_id: user_id, op: :weekly_notification}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp calculate_first_day_of_week(%Date{} = date, locale) do
|
||||
day_number = Date.day_of_week(date)
|
||||
first_day_number = Cldr.Calendar.first_day_for_locale(locale)
|
||||
|
||||
if day_number == first_day_number,
|
||||
do: date,
|
||||
else: calculate_first_day_of_week(Date.add(date, -1), locale)
|
||||
end
|
||||
end
|
||||
|
@ -207,4 +207,120 @@ defmodule Mobilizon.Service.Workers.NotificationTest do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "A weekly_notification job sends an email" do
|
||||
test "if the user is still participating" do
|
||||
%User{id: user_id} = user = insert(:user)
|
||||
|
||||
settings =
|
||||
insert(:settings, user_id: user_id, notification_each_week: true, timezone: "Europe/Paris")
|
||||
|
||||
user = Map.put(user, :settings, settings)
|
||||
%Actor{} = actor = insert(:actor, user: user)
|
||||
|
||||
%Participant{} = participant = insert(:participant, role: :participant, actor: actor)
|
||||
|
||||
Notification.perform(
|
||||
%{"op" => "weekly_notification", "user_id" => user_id},
|
||||
nil
|
||||
)
|
||||
|
||||
assert_delivered_email(
|
||||
NotificationMailer.weekly_notification(
|
||||
user,
|
||||
[participant],
|
||||
1
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "unless the person is no longer participating" do
|
||||
%Event{id: event_id} = insert(:event)
|
||||
|
||||
%User{id: user_id} = user = insert(:user)
|
||||
|
||||
settings =
|
||||
insert(:settings, user_id: user_id, notification_each_week: true, timezone: "Europe/Paris")
|
||||
|
||||
user = Map.put(user, :settings, settings)
|
||||
%Actor{} = actor = insert(:actor, user: user)
|
||||
|
||||
{:ok, %Participant{} = participant} =
|
||||
Events.create_participant(%{actor_id: actor.id, event_id: event_id, role: :participant})
|
||||
|
||||
actor = Map.put(participant.actor, :user, user)
|
||||
participant = Map.put(participant, :actor, actor)
|
||||
|
||||
assert {:ok, %Participant{}} = Events.delete_participant(participant)
|
||||
|
||||
Notification.perform(
|
||||
%{"op" => "weekly_notification", "user_id" => user_id},
|
||||
nil
|
||||
)
|
||||
|
||||
refute_delivered_email(
|
||||
NotificationMailer.weekly_notification(
|
||||
user,
|
||||
[participant],
|
||||
1
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "unless the event has been cancelled" do
|
||||
%User{id: user_id} = user = insert(:user)
|
||||
|
||||
settings =
|
||||
insert(:settings, user_id: user_id, notification_each_week: true, timezone: "Europe/Paris")
|
||||
|
||||
user = Map.put(user, :settings, settings)
|
||||
%Actor{} = actor = insert(:actor, user: user)
|
||||
%Event{} = event = insert(:event, status: :cancelled)
|
||||
|
||||
%Participant{} =
|
||||
participant = insert(:participant, role: :participant, event: event, actor: actor)
|
||||
|
||||
Notification.perform(
|
||||
%{"op" => "weekly_notification", "user_id" => user_id},
|
||||
nil
|
||||
)
|
||||
|
||||
refute_delivered_email(
|
||||
NotificationMailer.weekly_notification(
|
||||
user,
|
||||
[participant],
|
||||
1
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "with a lot of events" do
|
||||
%User{id: user_id} = user = insert(:user)
|
||||
|
||||
settings =
|
||||
insert(:settings, user_id: user_id, notification_each_week: true, timezone: "Europe/Paris")
|
||||
|
||||
user = Map.put(user, :settings, settings)
|
||||
%Actor{} = actor = insert(:actor, user: user)
|
||||
|
||||
participants =
|
||||
Enum.reduce(0..10, [], fn _i, acc ->
|
||||
%Participant{} = participant = insert(:participant, role: :participant, actor: actor)
|
||||
acc ++ [participant]
|
||||
end)
|
||||
|
||||
Notification.perform(
|
||||
%{"op" => "weekly_notification", "user_id" => user_id},
|
||||
nil
|
||||
)
|
||||
|
||||
refute_delivered_email(
|
||||
NotificationMailer.weekly_notification(
|
||||
user,
|
||||
participants,
|
||||
3
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user