Add weekly notification

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-06-05 10:12:08 +02:00
parent 03b1f84fba
commit 44e08c4319
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
8 changed files with 440 additions and 18 deletions

View file

@ -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

View file

@ -28,28 +28,54 @@ 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

View file

@ -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

View file

@ -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>

View 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>

View 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 %>

View file

@ -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

View file

@ -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