Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@
transform: rotate(-3deg);
}
}
/* Define the root bg color to be a slightly darker color than the base 100 used for contrast boxes */
--root-bg: var(--color-base-200);
}

@layer base {
Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ config :admin, Oban,
],
engine: Oban.Engines.Basic,
notifier: Oban.Notifiers.Postgres,
queues: [default: 10, mailers: 1],
queues: [default: 10, mailing: 2],
repo: Admin.Repo

config :admin, :scopes,
Expand Down
10 changes: 10 additions & 0 deletions lib/admin/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -389,13 +389,23 @@ defmodule Admin.Accounts do
def get_active_members do
Repo.all(
from(m in Account,
select: %{name: m.name, email: m.email, lang: fragment("?->>?", m.extra, "lang")},
where:
not is_nil(m.last_authenticated_at) and m.last_authenticated_at > ago(90, "day") and
m.type == "individual"
)
)
end

def get_members_by_language(language) do
Repo.all(
from(m in Account,
select: %{name: m.name, email: m.email, lang: fragment("?->>?", m.extra, "lang")},
where: fragment("?->>? = ?", m.extra, "lang", ^language) and m.type == "individual"
)
)
end

def create_member(attrs \\ %{}) do
%Account{}
|> Account.changeset(attrs)
Expand Down
2 changes: 2 additions & 0 deletions lib/admin/accounts/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defmodule Admin.Accounts.Account do
field :name, :string
field :email, :string
field :type, :string
field :extra, :map
field :last_authenticated_at, :utc_datetime

timestamps(type: :utc_datetime)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/admin/accounts/scope.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

defstruct user: nil

@type t :: %__MODULE__{user: User.t() | nil}

Check warning on line 23 in lib/admin/accounts/scope.ex

View workflow job for this annotation

GitHub Actions / Test on OTP 28.0.2 / Elixir 1.19.4

unknown_type

Unknown type: Admin.Accounts.User.t/0.

@doc """
Creates a scope for the given user.

Expand Down
30 changes: 30 additions & 0 deletions lib/admin/accounts/user_notifier.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,36 @@ defmodule Admin.Accounts.UserNotifier do
)
end

def deliver_call_to_action(user, subject, message_text, button_text, button_url) do
html_body =
EmailTemplates.render("call_to_action", %{
name: user.name,
message: message_text,
button_text: button_text,
button_url: button_url
})

deliver(
user.email,
subject,
html_body,
"""

==============================

Hi #{user.name},

#{message_text}

#{button_text} #{button_url}

==============================
#{@footer}
""",
reply_to: @support_email
)
end

@doc """
Deliver publication removal information.
"""
Expand Down
65 changes: 0 additions & 65 deletions lib/admin/mailer_worker.ex

This file was deleted.

115 changes: 115 additions & 0 deletions lib/admin/mailing_worker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
defmodule Admin.MailingWorker do
@moduledoc """
Worker for sending batch emails to a target audience with internationalisation.
"""

use Oban.Worker, queue: :mailing

alias Admin.Accounts
alias Admin.Accounts.Scope
alias Admin.Accounts.UserNotifier
alias Admin.Notifications

@impl Oban.Worker
def perform(%Oban.Job{
args:
%{
"user_id" => user_id,
"notification_id" => notification_id
} =
_args
}) do
user = Accounts.get_user!(user_id)
scope = Scope.for_user(user)

with {:ok, notification} <- Notifications.get_notification(scope, notification_id),
included_langs = notification.localized_emails |> Enum.map(& &1.language),
{:ok, audience} <-
Notifications.get_target_audience(
scope,
notification.audience,
if(notification.use_strict_languages, do: [only_langs: included_langs], else: [])
) do
# save number of recipients to the notification
Notifications.update_recipients(notification, %{total_recipients: length(audience)})
# start sending emails
send_emails(scope, notification, audience)
# await email progress messages
await_emails(scope, notification)
else
{:error, :notification_not_found} ->
{:cancel, :notification_not_found}

{:error, error} ->
{:error, "Failed to send notification: #{inspect(error)}"}
end
end

defp send_emails(scope, notification, audience) do
job_pid = self()

Task.async(fn ->
audience
|> Enum.with_index(1)
|> Enum.each(fn {user, index} ->
send_local_email(scope, user, notification)

current_progress = trunc(index / length(audience) * 100)

send(job_pid, {:progress, current_progress})

:timer.sleep(1000)
end)

send(job_pid, {:completed})
end)
end

defp send_local_email(scope, user, notification) do
# get the localized email
case Notifications.get_local_email_from_notification(notification, user.lang) do
nil ->
:skipped

localized_email ->
# deliver the email
UserNotifier.deliver_call_to_action(
user,
localized_email.subject,
localized_email.message,
localized_email.button_text,
localized_email.button_url
)

# save message log
Notifications.save_log(
scope,
%{
email: user.email,
status: "sent"
},
notification
)

:ok
end
end

defp await_emails(scope, notification) do
receive do
{:progress, percent} ->
Notifications.report_sending_progress(scope, {:progress, notification.name, percent})
await_emails(scope, notification)

{:completed} ->
Notifications.report_sending_progress(scope, {:completed, notification.name})

{:failed} ->
Notifications.report_sending_progress(scope, {:failed, notification.name})
after
30_000 ->
Notifications.report_sending_progress(scope, {:failed, notification.name})
raise RuntimeError, "no progress after 30s"
end
end
end
Loading
Loading