diff --git a/lib/service/notifications/scheduler.ex b/lib/service/notifications/scheduler.ex
index f4834a479..c288fcdcf 100644
--- a/lib/service/notifications/scheduler.ex
+++ b/lib/service/notifications/scheduler.ex
@@ -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
diff --git a/lib/service/workers/notification.ex b/lib/service/workers/notification.ex
index 3a9a76701..099bf3245 100644
--- a/lib/service/workers/notification.ex
+++ b/lib/service/workers/notification.ex
@@ -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
diff --git a/lib/web/email/notification.ex b/lib/web/email/notification.ex
index 7d14185e6..0684f271a 100644
--- a/lib/web/email/notification.ex
+++ b/lib/web/email/notification.ex
@@ -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
diff --git a/lib/web/templates/email/email.html.eex b/lib/web/templates/email/email.html.eex
index eef14eb44..8e1e61855 100644
--- a/lib/web/templates/email/email.html.eex
+++ b/lib/web/templates/email/email.html.eex
@@ -42,10 +42,6 @@
-
-
diff --git a/lib/web/templates/email/notification_each_week.html.eex b/lib/web/templates/email/notification_each_week.html.eex
new file mode 100644
index 000000000..3f3a30ac5
--- /dev/null
+++ b/lib/web/templates/email/notification_each_week.html.eex
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+ <%= 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 %>
+
+
+ <% end %>
+
+ <% else %>
+
+ <%= @participation.event.begins_on |> DateTime.shift_zone!(@timezone) |> datetime_to_string(@locale) %>
+
+
+ <%= @participation.event.title %>
+
+ <% 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 %>
+
+ |
+
+
+
+ |
+
diff --git a/lib/web/templates/email/notification_each_week.text.eex b/lib/web/templates/email/notification_each_week.text.eex
new file mode 100644
index 000000000..975e6d539
--- /dev/null
+++ b/lib/web/templates/email/notification_each_week.text.eex
@@ -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 %>
diff --git a/test/service/notifications/scheduler_test.exs b/test/service/notifications/scheduler_test.exs
index d335ebb4a..966a07e55 100644
--- a/test/service/notifications/scheduler_test.exs
+++ b/test/service/notifications/scheduler_test.exs
@@ -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
diff --git a/test/service/workers/notification_test.exs b/test/service/workers/notification_test.exs
index d6cf1ec8c..a09211912 100644
--- a/test/service/workers/notification_test.exs
+++ b/test/service/workers/notification_test.exs
@@ -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