Add email notifications
This commit is contained in:
parent
ebc987496f
commit
58326cada3
11 changed files with 167 additions and 1 deletions
|
@ -31,6 +31,18 @@ config :chore_tracker, ChoreTrackerWeb.Endpoint,
|
|||
# at the `config/runtime.exs`.
|
||||
config :chore_tracker, ChoreTracker.Mailer, adapter: Swoosh.Adapters.Local
|
||||
|
||||
config :chore_tracker, Oban,
|
||||
engine: Oban.Engines.Basic,
|
||||
queues: [default: 10, notifications: 10],
|
||||
repo: ChoreTracker.Repo,
|
||||
plugins: [
|
||||
{Oban.Plugins.Cron,
|
||||
crontab: [
|
||||
{"10 0 * * *", ChoreTracker.Notifications.NotificationScheduler}
|
||||
]},
|
||||
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7}
|
||||
]
|
||||
|
||||
# Configure esbuild (the version is required)
|
||||
config :esbuild,
|
||||
version: "0.17.11",
|
||||
|
|
|
@ -114,4 +114,7 @@ if config_env() == :prod do
|
|||
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
|
||||
#
|
||||
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
|
||||
|
||||
config :chore_tracker, ChoreTracker.Notifications,
|
||||
email_from: System.get_env("NOTIFICATIONS_EMAIL_FROM")
|
||||
end
|
||||
|
|
|
@ -26,6 +26,9 @@ config :chore_tracker, ChoreTrackerWeb.Endpoint,
|
|||
# In test we don't send emails.
|
||||
config :chore_tracker, ChoreTracker.Mailer, adapter: Swoosh.Adapters.Test
|
||||
|
||||
# To prevent Oban from running jobs and plugins during test runs, enable :testing mode in test.exs:
|
||||
config :chore_tracker, Oban, testing: :inline
|
||||
|
||||
# Disable swoosh api client as it is only required for production adapters.
|
||||
config :swoosh, :api_client, false
|
||||
|
||||
|
|
|
@ -7,9 +7,12 @@ defmodule ChoreTracker.Application do
|
|||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
Oban.Telemetry.attach_default_logger()
|
||||
|
||||
children = [
|
||||
ChoreTrackerWeb.Telemetry,
|
||||
ChoreTracker.Repo,
|
||||
{Oban, Application.fetch_env!(:chore_tracker, Oban)},
|
||||
{DNSCluster, query: Application.get_env(:chore_tracker, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: ChoreTracker.PubSub},
|
||||
# Start the Finch HTTP client for sending emails
|
||||
|
|
50
lib/chore_tracker/notifications/notification.ex
Normal file
50
lib/chore_tracker/notifications/notification.ex
Normal file
|
@ -0,0 +1,50 @@
|
|||
defmodule ChoreTracker.Notifications.Notification do
|
||||
use Oban.Worker, queue: :notifications
|
||||
|
||||
alias ChoreTracker.Accounts
|
||||
alias ChoreTracker.Mailer
|
||||
alias ChoreTracker.Chores
|
||||
alias Swoosh.Email
|
||||
|
||||
@impl true
|
||||
def perform(%Oban.Job{} = job) do
|
||||
%{"chore_id" => chore_id, "assignee_id" => assignee_id, "kind" => kind} = job.args
|
||||
|
||||
chore = Chores.get_chore!(chore_id)
|
||||
assignee = Accounts.get_user!(assignee_id)
|
||||
text = build_mail(kind, chore)
|
||||
|
||||
from =
|
||||
Application.get_env(:chore_tracker, ChoreTracker.Notifications, "notifications@example.com")
|
||||
|
||||
Email.new()
|
||||
|> Email.from(from)
|
||||
|> Email.to(assignee.email)
|
||||
|> Email.text_body(text)
|
||||
|> Mailer.deliver()
|
||||
end
|
||||
|
||||
defp build_mail("next_week", chore) do
|
||||
"""
|
||||
Za týden máš udělat úkol: #{chore.name}
|
||||
|
||||
#{chore.description}
|
||||
"""
|
||||
end
|
||||
|
||||
defp build_mail("today", chore) do
|
||||
"""
|
||||
Dnes máš udělat úkol: #{chore.name}
|
||||
|
||||
#{chore.description}
|
||||
"""
|
||||
end
|
||||
|
||||
defp build_mail("overdue", chore) do
|
||||
"""
|
||||
Už jsi měl udělat úkol: #{chore.name}
|
||||
|
||||
#{chore.description}
|
||||
"""
|
||||
end
|
||||
end
|
42
lib/chore_tracker/notifications/notification_scheduler.ex
Normal file
42
lib/chore_tracker/notifications/notification_scheduler.ex
Normal file
|
@ -0,0 +1,42 @@
|
|||
defmodule ChoreTracker.Notifications.NotificationScheduler do
|
||||
use Oban.Worker
|
||||
|
||||
alias ChoreTracker.Chores
|
||||
alias ChoreTracker.Chores.Chore
|
||||
alias ChoreTracker.Notifications.Notification
|
||||
|
||||
@moduledoc """
|
||||
This workers runs daily at specified time (time can be configured in Oban crontab in `config/config.ex`).
|
||||
It runs through all chores and decides if their assignees should be notified.
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def perform(_job) do
|
||||
chores = Chores.list_assigned_chores()
|
||||
|
||||
for chore <- chores do
|
||||
kind = Date.diff(chore.scheduled_at, Date.utc_today()) |> notification_kind()
|
||||
schedule_notification(kind, chore)
|
||||
end
|
||||
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
# Specify notification type according to number of days to next scheduled execution time.
|
||||
defp notification_kind(in_days) do
|
||||
case in_days do
|
||||
7 -> :next_week
|
||||
0 -> :today
|
||||
diff when diff < 0 -> :overdue
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp schedule_notification(nil, _), do: nil
|
||||
|
||||
defp schedule_notification(kind, %Chore{} = chore) do
|
||||
%{chore_id: chore.id, assignee_id: chore.next_assignee_id, kind: kind}
|
||||
|> Notification.new()
|
||||
|> Oban.insert()
|
||||
end
|
||||
end
|
3
mix.exs
3
mix.exs
|
@ -60,7 +60,8 @@ defmodule ChoreTracker.MixProject do
|
|||
{:dns_cluster, "~> 0.1.1"},
|
||||
{:bandit, "~> 1.2"},
|
||||
{:earmark, "~> 1.4"},
|
||||
{:ex_cldr_dates_times, "~> 2.0"}
|
||||
{:ex_cldr_dates_times, "~> 2.0"},
|
||||
{:oban, "~> 2.17"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -30,6 +30,7 @@
|
|||
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"oban": {:hex, :oban, "2.18.0", "092d20bfd3d70c7ecb70960f8548d300b54bb9937c7f2e56b388f3a9ed02ec68", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aace1eff6f8227ae38d4274af967d96f051c2f0a5152f2ef9809dd1f97866745"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
|
||||
|
|
13
priv/repo/migrations/20240814192205_add_oban_jobs_table.exs
Normal file
13
priv/repo/migrations/20240814192205_add_oban_jobs_table.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule ChoreTracker.Repo.Migrations.AddObanJobsTable do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
Oban.Migration.up(version: 12)
|
||||
end
|
||||
|
||||
# We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if
|
||||
# necessary, regardless of which version we've migrated `up` to.
|
||||
def down do
|
||||
Oban.Migration.down(version: 1)
|
||||
end
|
||||
end
|
29
test/chore_tracker/notifications_test.exs
Normal file
29
test/chore_tracker/notifications_test.exs
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule ChoreTracker.NotificationsTest do
|
||||
use ChoreTracker.DataCase
|
||||
use ChoreTracker.ObanCase
|
||||
|
||||
import ChoreTracker.AccountsFixtures
|
||||
import ChoreTracker.ChoresFixtures
|
||||
|
||||
alias ChoreTracker.Notifications.NotificationScheduler
|
||||
|
||||
setup do
|
||||
users =
|
||||
for username <- ["socrates", "plato", "aristotle"] do
|
||||
user_fixture(user_login(username))
|
||||
end
|
||||
|
||||
%{users: users}
|
||||
end
|
||||
|
||||
test "runs notification scheduler", %{users: users} do
|
||||
today = Date.utc_today()
|
||||
|
||||
chore_fixture(%{assignees: users, scheduled_at: Date.add(today, 12)})
|
||||
chore_fixture(%{assignees: users, scheduled_at: Date.add(today, 7)})
|
||||
chore_fixture(%{assignees: users, scheduled_at: today})
|
||||
chore_fixture(%{assignees: users, scheduled_at: Date.add(today, -1)})
|
||||
|
||||
{:ok, nil} = perform_job(NotificationScheduler, %{})
|
||||
end
|
||||
end
|
9
test/support/oban_case.ex
Normal file
9
test/support/oban_case.ex
Normal file
|
@ -0,0 +1,9 @@
|
|||
defmodule ChoreTracker.ObanCase do
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
using do
|
||||
quote do
|
||||
use Oban.Testing, repo: ChoreTracker.Repo
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue