diff --git a/apps/transport/lib/db/notification_subscription.ex b/apps/transport/lib/db/notification_subscription.ex
index 3dabcbc87e..739fb2fcf6 100644
--- a/apps/transport/lib/db/notification_subscription.ex
+++ b/apps/transport/lib/db/notification_subscription.ex
@@ -14,14 +14,18 @@ defmodule DB.NotificationSubscription do
@hidden_reasons_related_to_datasets [:dataset_now_on_nap, :resources_changed]
# These notification reasons are *not* linked to a specific dataset, `dataset_id` should be nil
@platform_wide_reasons [:new_dataset, :datasets_switching_climate_resilience_bill, :daily_new_comments]
+ @possible_roles [:producer, :reuser]
+ @type role :: :producer | :reuser
typed_schema "notification_subscription" do
field(:reason, Ecto.Enum,
values: @reasons_related_to_datasets ++ @platform_wide_reasons ++ @hidden_reasons_related_to_datasets
)
+ # Useful to know if the subscription has been created by an admin
+ # from the backoffice (`:admin`) or by the user (`:user`)
field(:source, Ecto.Enum, values: [:admin, :user])
- field(:role, Ecto.Enum, values: [:producer, :reuser])
+ field(:role, Ecto.Enum, values: @possible_roles)
belongs_to(:contact, DB.Contact)
belongs_to(:dataset, DB.Dataset)
@@ -83,6 +87,26 @@ defmodule DB.NotificationSubscription do
|> DB.Repo.all()
end
+ @spec subscriptions_for_reason_dataset_and_role(atom(), DB.Dataset.t(), role()) :: [__MODULE__.t()]
+ def subscriptions_for_reason_dataset_and_role(reason, %DB.Dataset{id: dataset_id}, role)
+ when role in @possible_roles do
+ base_query()
+ |> preload([:contact])
+ |> where(
+ [notification_subscription: ns],
+ ns.reason == ^reason and ns.dataset_id == ^dataset_id and ns.role == ^role
+ )
+ |> DB.Repo.all()
+ end
+
+ @spec subscriptions_for_reason_and_role(atom(), role()) :: [__MODULE__.t()]
+ def subscriptions_for_reason_and_role(reason, role) when role in @possible_roles do
+ base_query()
+ |> preload([:contact])
+ |> where([notification_subscription: ns], ns.reason == ^reason and is_nil(ns.dataset_id) and ns.role == ^role)
+ |> DB.Repo.all()
+ end
+
@spec subscriptions_for_dataset(DB.Dataset.t()) :: [__MODULE__.t()]
def subscriptions_for_dataset(%DB.Dataset{id: dataset_id}) do
base_query()
@@ -91,6 +115,14 @@ defmodule DB.NotificationSubscription do
|> DB.Repo.all()
end
+ @spec subscriptions_for_dataset_and_role(DB.Dataset.t(), role()) :: [__MODULE__.t()]
+ def subscriptions_for_dataset_and_role(%DB.Dataset{id: dataset_id}, role) when role in @possible_roles do
+ base_query()
+ |> preload([:contact])
+ |> where([notification_subscription: ns], ns.dataset_id == ^dataset_id and ns.role == ^role)
+ |> DB.Repo.all()
+ end
+
@spec subscriptions_to_emails([__MODULE__.t()]) :: [binary()]
def subscriptions_to_emails(subscriptions) do
subscriptions |> Enum.map(& &1.contact.email)
diff --git a/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex b/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex
index 31f70eaa6d..49b4cbf18a 100644
--- a/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex
+++ b/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex
@@ -26,40 +26,42 @@ defmodule Transport.Jobs.MultiValidationWithErrorNotificationJob do
inserted_at
|> relevant_validations()
|> Enum.each(fn {%DB.Dataset{} = dataset, multi_validations} ->
- dataset
- |> emails_list()
- |> MapSet.difference(notifications_sent_recently(dataset))
- |> Enum.each(fn email ->
- Transport.EmailSender.impl().send_mail(
- "transport.data.gouv.fr",
- Application.get_env(:transport, :contact_email),
- email,
- Application.get_env(:transport, :contact_email),
- "Erreurs détectées dans le jeu de données #{dataset.custom_title}",
- """
- Bonjour,
+ producer_emails = dataset |> emails_list(:producer)
+ send_to_producers(producer_emails, dataset, multi_validations)
- Des erreurs bloquantes ont été détectées dans votre jeu de données #{dataset_url(dataset)}. Ces erreurs empêchent la réutilisation de vos données.
-
- Nous vous invitons à les corriger en vous appuyant sur les rapports de validation suivants :
- #{Enum.map_join(multi_validations, "\n", &resource_link/1)}
-
- Nous restons disponible pour vous accompagner si besoin.
-
- Merci par avance pour votre action,
+ reuser_emails = dataset |> emails_list(:reuser)
+ send_to_reusers(reuser_emails, dataset, producer_warned: not Enum.empty?(producer_emails))
+ end)
+ end
- À bientôt,
+ defp send_to_reusers(emails, %DB.Dataset{} = dataset, producer_warned: producer_warned) do
+ Enum.each(emails, &send_mail(&1, :reuser, dataset: dataset, producer_warned: producer_warned))
+ end
- L'équipe du PAN
- """,
- ""
- )
+ defp send_to_producers(emails, %DB.Dataset{} = dataset, multi_validations) do
+ Enum.each(
+ emails,
+ &send_mail(&1, :producer,
+ dataset: dataset,
+ resources: Enum.map(multi_validations, fn mv -> mv.resource_history.resource end)
+ )
+ )
+ end
- save_notification(dataset, email)
- end)
- end)
+ defp send_mail(email, role, args) do
+ dataset = Keyword.fetch!(args, :dataset)
+
+ Transport.EmailSender.impl().send_mail(
+ "transport.data.gouv.fr",
+ Application.get_env(:transport, :contact_email),
+ email,
+ Application.get_env(:transport, :contact_email),
+ "Erreurs détectées dans le jeu de données #{dataset.custom_title}",
+ "",
+ Phoenix.View.render_to_string(TransportWeb.EmailView, "#{@notification_reason}_#{role}.html", args)
+ )
- :ok
+ save_notification(dataset, email)
end
defp notifications_sent_recently(%DB.Dataset{id: dataset_id}) do
@@ -91,24 +93,11 @@ defmodule Transport.Jobs.MultiValidationWithErrorNotificationJob do
|> Enum.group_by(& &1.resource_history.resource.dataset)
end
- defp emails_list(%DB.Dataset{} = dataset) do
+ defp emails_list(%DB.Dataset{} = dataset, role) do
@notification_reason
- |> DB.NotificationSubscription.subscriptions_for_reason(dataset)
+ |> DB.NotificationSubscription.subscriptions_for_reason_dataset_and_role(dataset, role)
|> DB.NotificationSubscription.subscriptions_to_emails()
|> MapSet.new()
- end
-
- defp dataset_url(%DB.Dataset{slug: slug, custom_title: custom_title}) do
- url = TransportWeb.Router.Helpers.dataset_url(TransportWeb.Endpoint, :details, slug)
-
- "#{custom_title} — #{url}"
- end
-
- defp resource_link(%DB.MultiValidation{
- resource_history: %DB.ResourceHistory{resource: %DB.Resource{id: id, title: title}}
- }) do
- url = TransportWeb.Router.Helpers.resource_url(TransportWeb.Endpoint, :details, id) <> "#validation-report"
-
- "* #{title} — #{url}"
+ |> MapSet.difference(notifications_sent_recently(dataset))
end
end
diff --git a/apps/transport/lib/jobs/resource_unavailable_notification_job.ex b/apps/transport/lib/jobs/resource_unavailable_notification_job.ex
index eae5dd97e1..f3c029c895 100644
--- a/apps/transport/lib/jobs/resource_unavailable_notification_job.ex
+++ b/apps/transport/lib/jobs/resource_unavailable_notification_job.ex
@@ -21,30 +21,50 @@ defmodule Transport.Jobs.ResourceUnavailableNotificationJob do
inserted_at
|> relevant_unavailabilities()
|> Enum.each(fn {%DB.Dataset{} = dataset, unavailabilities} ->
- dataset
- |> emails_list()
- |> MapSet.difference(notifications_sent_recently(dataset))
- |> Enum.each(fn email ->
- Transport.EmailSender.impl().send_mail(
- "transport.data.gouv.fr",
- Application.get_env(:transport, :contact_email),
- email,
- Application.get_env(:transport, :contact_email),
- "Ressources indisponibles dans le jeu de données #{dataset.custom_title}",
- "",
- Phoenix.View.render_to_string(TransportWeb.EmailView, "resource_unavailable.html",
- dataset: dataset,
- hours_consecutive_downtime: @hours_consecutive_downtime,
- deleted_recreated_on_datagouv: deleted_and_recreated_resource_hosted_on_datagouv(dataset, unavailabilities),
- resource_titles: Enum.map_join(unavailabilities, ", ", &resource_title/1)
- )
- )
-
- save_notification(dataset, email)
- end)
+ producer_emails = emails_list(dataset, :producer)
+ send_to_producers(producer_emails, dataset, unavailabilities)
+
+ reuser_emails = emails_list(dataset, :reuser)
+ send_to_reusers(reuser_emails, dataset, unavailabilities, producer_warned: not Enum.empty?(producer_emails))
+ end)
+ end
+
+ defp send_to_reusers(emails, %DB.Dataset{} = dataset, unavailabilities, producer_warned: producer_warned) do
+ Enum.each(emails, fn email ->
+ send_mail(email, :reuser,
+ dataset: dataset,
+ hours_consecutive_downtime: @hours_consecutive_downtime,
+ producer_warned: producer_warned,
+ resource_titles: Enum.map_join(unavailabilities, ", ", &resource_title/1)
+ )
+ end)
+ end
+
+ defp send_to_producers(emails, dataset, unavailabilities) do
+ Enum.each(emails, fn email ->
+ send_mail(email, :producer,
+ dataset: dataset,
+ hours_consecutive_downtime: @hours_consecutive_downtime,
+ deleted_recreated_on_datagouv: deleted_and_recreated_resource_hosted_on_datagouv(dataset, unavailabilities),
+ resource_titles: Enum.map_join(unavailabilities, ", ", &resource_title/1)
+ )
end)
+ end
+
+ defp send_mail(email, role, args) do
+ dataset = Keyword.fetch!(args, :dataset)
+
+ Transport.EmailSender.impl().send_mail(
+ "transport.data.gouv.fr",
+ Application.get_env(:transport, :contact_email),
+ email,
+ Application.get_env(:transport, :contact_email),
+ "Ressources indisponibles dans le jeu de données #{dataset.custom_title}",
+ "",
+ Phoenix.View.render_to_string(TransportWeb.EmailView, "#{@notification_reason}_#{role}.html", args)
+ )
- :ok
+ save_notification(dataset, email)
end
defp notifications_sent_recently(%DB.Dataset{id: dataset_id}) do
@@ -105,11 +125,12 @@ defmodule Transport.Jobs.ResourceUnavailableNotificationJob do
DB.Notification.insert!(@notification_reason, dataset, email)
end
- defp emails_list(%DB.Dataset{} = dataset) do
+ defp emails_list(%DB.Dataset{} = dataset, role) do
@notification_reason
- |> DB.NotificationSubscription.subscriptions_for_reason(dataset)
+ |> DB.NotificationSubscription.subscriptions_for_reason_dataset_and_role(dataset, role)
|> DB.NotificationSubscription.subscriptions_to_emails()
|> MapSet.new()
+ |> MapSet.difference(notifications_sent_recently(dataset))
end
defp resource_title(%DB.ResourceUnavailability{resource: %DB.Resource{title: title}}) do
diff --git a/apps/transport/lib/transport_web/templates/email/dataset_with_error_producer.html.md b/apps/transport/lib/transport_web/templates/email/dataset_with_error_producer.html.md
new file mode 100644
index 0000000000..d5c04469c4
--- /dev/null
+++ b/apps/transport/lib/transport_web/templates/email/dataset_with_error_producer.html.md
@@ -0,0 +1,16 @@
+Bonjour,
+
+Des erreurs bloquantes ont été détectées dans votre jeu de données <%= link_for_dataset(@dataset, :heex) %>. Ces erreurs empêchent la réutilisation de vos données.
+
+Nous vous invitons à les corriger en vous appuyant sur les rapports de validation suivants :
+<%= for resource <- @resources do %>
+<%= link_for_resource(resource) %>
+<% end %>
+
+Nous restons disponible pour vous accompagner si besoin.
+
+Merci par avance pour votre action,
+
+À bientôt,
+
+L'équipe du PAN
diff --git a/apps/transport/lib/transport_web/templates/email/dataset_with_error_reuser.html.md b/apps/transport/lib/transport_web/templates/email/dataset_with_error_reuser.html.md
new file mode 100644
index 0000000000..14f22e7f5f
--- /dev/null
+++ b/apps/transport/lib/transport_web/templates/email/dataset_with_error_reuser.html.md
@@ -0,0 +1,9 @@
+Bonjour,
+
+Des erreurs bloquantes ont été détectées dans le jeu de données <%= link_for_dataset(@dataset, :heex) %> que vous réutilisez.
+
+<%= if @producer_warned do %>
+Le producteur de ces données a été informé de ces erreurs.
+<% end %>
+
+L’équipe du PAN
diff --git a/apps/transport/lib/transport_web/templates/email/resource_unavailable.html.md b/apps/transport/lib/transport_web/templates/email/resource_unavailable_producer.html.md
similarity index 100%
rename from apps/transport/lib/transport_web/templates/email/resource_unavailable.html.md
rename to apps/transport/lib/transport_web/templates/email/resource_unavailable_producer.html.md
diff --git a/apps/transport/lib/transport_web/templates/email/resource_unavailable_reuser.html.md b/apps/transport/lib/transport_web/templates/email/resource_unavailable_reuser.html.md
new file mode 100644
index 0000000000..575a240626
--- /dev/null
+++ b/apps/transport/lib/transport_web/templates/email/resource_unavailable_reuser.html.md
@@ -0,0 +1,9 @@
+Bonjour,
+
+Les ressources <%= @resource_titles %> du jeu de données <%= link_for_dataset(@dataset, :heex) %> que vous réutilisez ne sont plus disponibles au téléchargement depuis plus de <%= @hours_consecutive_downtime %>h.
+
+<%= if @producer_warned do %>
+Le producteur de ces données a été informé de cette indisponibilité.
+<% end %>
+
+L’équipe du PAN
diff --git a/apps/transport/lib/transport_web/views/email_view.ex b/apps/transport/lib/transport_web/views/email_view.ex
index 3ba95101b7..7cf2268096 100644
--- a/apps/transport/lib/transport_web/views/email_view.ex
+++ b/apps/transport/lib/transport_web/views/email_view.ex
@@ -9,4 +9,9 @@ defmodule TransportWeb.EmailView do
:heex -> link(custom_title, to: url)
end
end
+
+ def link_for_resource(%DB.Resource{id: id, title: title}) do
+ url = TransportWeb.Router.Helpers.resource_url(TransportWeb.Endpoint, :details, id)
+ link(title, to: url)
+ end
end
diff --git a/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs b/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs
index 91bab44c85..088174a933 100644
--- a/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs
+++ b/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs
@@ -91,7 +91,9 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT
%DB.Contact{id: already_sent_contact_id} = insert_contact(%{email: already_sent_email})
%DB.Contact{id: foo_contact_id} = insert_contact(%{email: "foo@example.com"})
+ %DB.Contact{id: reuser_contact_id} = insert_contact(%{email: reuser_email = "reuser@example.com"})
+ # Subscriptions for a contact who was already warned, a producer and a reuser
insert(:notification_subscription, %{
reason: :dataset_with_error,
source: :admin,
@@ -108,6 +110,15 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT
dataset_id: dataset.id
})
+ insert(:notification_subscription, %{
+ reason: :dataset_with_error,
+ source: :user,
+ role: :reuser,
+ contact_id: reuser_contact_id,
+ dataset_id: dataset.id
+ })
+
+ # Contact + subscription for another dataset
%DB.Contact{id: bar_contact_id} = insert_contact(%{email: "bar@example.com"})
insert(:notification_subscription, %{
@@ -124,18 +135,36 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT
"foo@example.com" = _to,
"contact@transport.beta.gouv.fr",
subject,
- plain_text_body,
- "" = _html_part ->
+ "",
+ html ->
assert subject == "Erreurs détectées dans le jeu de données #{dataset.custom_title}"
- assert plain_text_body =~
- "Des erreurs bloquantes ont été détectées dans votre jeu de données #{dataset.custom_title}"
+ assert html =~
+ ~s(Des erreurs bloquantes ont été détectées dans votre jeu de données #{dataset.custom_title})
+
+ assert html =~
+ ~s(#{resource_1.title})
+
+ assert html =~
+ ~s(#{resource_2.title})
+
+ :ok
+ end)
+
+ Transport.EmailSender.Mock
+ |> expect(:send_mail, fn "transport.data.gouv.fr",
+ "contact@transport.beta.gouv.fr",
+ ^reuser_email,
+ "contact@transport.beta.gouv.fr",
+ subject,
+ "",
+ html ->
+ assert subject == "Erreurs détectées dans le jeu de données #{dataset.custom_title}"
- assert plain_text_body =~
- "#{resource_1.title} — http://127.0.0.1:5100/resources/#{resource_1.id}#validation-report"
+ assert html =~
+ ~s(Des erreurs bloquantes ont été détectées dans le jeu de données #{dataset.custom_title} que vous réutilisez.)
- assert plain_text_body =~
- "#{resource_2.title} — http://127.0.0.1:5100/resources/#{resource_2.id}#validation-report"
+ assert html =~ "Le producteur de ces données a été informé de ces erreurs."
:ok
end)
@@ -146,15 +175,15 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT
"bar@example.com" = _to,
"contact@transport.beta.gouv.fr",
subject,
- plain_text_body,
- "" = _html_part ->
+ "",
+ html ->
assert subject == "Erreurs détectées dans le jeu de données #{gtfs_dataset.custom_title}"
- assert plain_text_body =~
- "Des erreurs bloquantes ont été détectées dans votre jeu de données #{gtfs_dataset.custom_title}"
+ assert html =~
+ ~s(Des erreurs bloquantes ont été détectées dans votre jeu de données #{gtfs_dataset.custom_title})
- assert plain_text_body =~
- "#{resource_gtfs.title} — http://127.0.0.1:5100/resources/#{resource_gtfs.id}#validation-report"
+ assert html =~
+ ~s(#{resource_gtfs.title})
:ok
end)
@@ -172,6 +201,14 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT
)
|> DB.Repo.exists?()
+ assert DB.Notification
+ |> where(
+ [n],
+ n.email_hash == ^reuser_email and n.dataset_id == ^dataset_id and n.inserted_at >= ^recent_dt and
+ n.reason == :dataset_with_error
+ )
+ |> DB.Repo.exists?()
+
assert DB.Notification
|> where(
[n],
diff --git a/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs b/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs
index 8de6267a3f..8bf55351c1 100644
--- a/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs
+++ b/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs
@@ -101,6 +101,7 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d
%DB.Contact{id: already_sent_contact_id} = insert_contact(%{email: already_sent_email})
%DB.Contact{id: foo_contact_id} = insert_contact(%{email: "foo@example.com"})
+ %DB.Contact{id: reuser_contact_id} = insert_contact(%{email: reuser_email = "reuser@example.com"})
insert(:notification_subscription, %{
reason: :resource_unavailable,
@@ -118,6 +119,14 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d
dataset_id: dataset.id
})
+ insert(:notification_subscription, %{
+ reason: :resource_unavailable,
+ source: :user,
+ role: :reuser,
+ contact_id: reuser_contact_id,
+ dataset_id: dataset.id
+ })
+
%DB.Contact{id: bar_contact_id} = insert_contact(%{email: "bar@example.com"})
insert(:notification_subscription, %{
@@ -147,6 +156,24 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d
:ok
end)
+ Transport.EmailSender.Mock
+ |> expect(:send_mail, fn "transport.data.gouv.fr",
+ "contact@transport.beta.gouv.fr",
+ ^reuser_email = _to,
+ "contact@transport.beta.gouv.fr",
+ subject,
+ _plain_text_body,
+ html_part ->
+ assert subject == "Ressources indisponibles dans le jeu de données #{dataset.custom_title}"
+
+ assert html_part =~
+ ~s(Les ressources #{resource_1.title}, #{resource_2.title} du jeu de données #{dataset.custom_title} que vous réutilisez ne sont plus disponibles au téléchargement depuis plus de 6h.)
+
+ assert html_part =~ "Le producteur de ces données a été informé de cette indisponibilité."
+
+ :ok
+ end)
+
Transport.EmailSender.Mock
|> expect(:send_mail, fn "transport.data.gouv.fr",
"contact@transport.beta.gouv.fr",
@@ -171,6 +198,8 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d
# Logs have been saved
recent_dt = DateTime.utc_now() |> DateTime.add(-1, :second)
+ assert DB.Notification |> DB.Repo.aggregate(:count) == 7
+
assert DB.Notification
|> where(
[n],
@@ -179,6 +208,14 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d
)
|> DB.Repo.exists?()
+ assert DB.Notification
+ |> where(
+ [n],
+ n.email_hash == ^reuser_email and n.dataset_id == ^dataset_id and n.inserted_at >= ^recent_dt and
+ n.reason == :resource_unavailable
+ )
+ |> DB.Repo.exists?()
+
assert DB.Notification
|> where(
[n],