diff --git a/apps/transport/client/stylesheets/app.scss b/apps/transport/client/stylesheets/app.scss
index 719e1b0823..59a248e296 100644
--- a/apps/transport/client/stylesheets/app.scss
+++ b/apps/transport/client/stylesheets/app.scss
@@ -33,6 +33,7 @@
@import 'components/stats';
@import 'components/tooltip';
@import 'components/validation';
+@import 'components/feedback';
// States: microinteractions...
@import 'states';
diff --git a/apps/transport/client/stylesheets/components/_feedback.scss b/apps/transport/client/stylesheets/components/_feedback.scss
new file mode 100644
index 0000000000..e4d27de832
--- /dev/null
+++ b/apps/transport/client/stylesheets/components/_feedback.scss
@@ -0,0 +1,40 @@
+.feedback-selector {
+ input {
+ display: none;
+ }
+
+ label {
+ cursor: pointer;
+ display: inline-block;
+ margin-right: 12px;
+ filter: brightness(1.8) grayscale(1) opacity(0.7);
+ }
+
+ label:hover {
+ filter: brightness(1.2) grayscale(0.5) opacity(0.9);
+ }
+
+ input:checked + label {
+ filter: none;
+ color: $primary;
+ }
+
+ .feedback-emojis {
+ font-size: 48px;
+ }
+}
+
+.feedback-form {
+ #full-feedback-form.hidden {
+ display: none;
+ }
+
+ .form__group + #full-feedback-form {
+ margin-top: var(--space-l);
+ }
+
+ .form-special-field {
+ // hide field used as honey-pot
+ display: none;
+ }
+}
diff --git a/apps/transport/lib/mailjet/email_sender.ex b/apps/transport/lib/mailjet/email_sender.ex
index db8fc7d2fe..0e915c45a6 100644
--- a/apps/transport/lib/mailjet/email_sender.ex
+++ b/apps/transport/lib/mailjet/email_sender.ex
@@ -24,7 +24,8 @@ defmodule Transport.EmailSender.Dummy do
A development-time implementation which just happens to log to console
"""
- def send_mail(_from_name, from_email, to_email, _reply_to, subject, _text_body, _html_body) do
- Logger.info("Would send email: from #{from_email} to #{to_email}, subject '#{subject}'")
+ def send_mail(_from_name, from_email, to_email, _reply_to, subject, text_body, _html_body) do
+ Logger.info("Would send email: from #{from_email} to #{to_email}, subject '#{subject}'\n#{text_body}")
+ {:ok, "dummy email sent"}
end
end
diff --git a/apps/transport/lib/transport_web/controllers/contact_controller.ex b/apps/transport/lib/transport_web/controllers/contact_controller.ex
index 5ba2e8ab23..e76ef74f66 100644
--- a/apps/transport/lib/transport_web/controllers/contact_controller.ex
+++ b/apps/transport/lib/transport_web/controllers/contact_controller.ex
@@ -14,6 +14,9 @@ defmodule TransportWeb.ContactController do
end
def send_mail(conn, %{"email" => email, "topic" => subject, "question" => question} = params) do
+ %{email: email, subject: subject, question: question} =
+ sanitize_inputs(%{email: email, subject: subject, question: question})
+
contact_email = TransportWeb.ContactEmail.contact(email, subject, question)
case Transport.Mailer.deliver(contact_email) do
@@ -30,12 +33,14 @@ defmodule TransportWeb.ContactController do
end
def send_mail(conn, params) do
- Logger.error("Bad parameters for sending email #{params}")
+ Logger.error("Bad parameters for sending email #{inspect(params)}")
conn
|> put_flash(:error, gettext("There has been an error, try again later"))
|> redirect(to: params["redirect_path"] || page_path(conn, :index))
end
+
+ defp sanitize_inputs(map), do: Map.new(map, fn {k, v} -> {k, v |> String.trim() |> HtmlSanitizeEx.strip_tags()} end)
end
defmodule TransportWeb.ContactEmail do
@@ -49,4 +54,31 @@ defmodule TransportWeb.ContactEmail do
|> subject(subject)
|> text_body(question)
end
+
+ def feedback(rating, explanation, email, feature) do
+ rating_t = %{"like" => "j’aime", "neutral" => "neutre", "dislike" => "mécontent"}
+
+ reply_email =
+ if email == "" do
+ Application.fetch_env!(:transport, :contact_email)
+ else
+ email
+ end
+
+ feedback_content = """
+ Vous avez un nouvel avis sur le PAN.
+ Fonctionnalité : #{feature}
+ Notation : #{rating_t[rating]}
+ Adresse e-mail : #{email}
+
+ Explication : #{explanation}
+ """
+
+ new()
+ |> from({"Formulaire feedback", Application.fetch_env!(:transport, :contact_email)})
+ |> to(Application.fetch_env!(:transport, :contact_email))
+ |> reply_to(reply_email)
+ |> subject("Nouvel avis pour #{feature} : #{rating_t[rating]}")
+ |> text_body(feedback_content)
+ end
end
diff --git a/apps/transport/lib/transport_web/live/feedback_live.ex b/apps/transport/lib/transport_web/live/feedback_live.ex
new file mode 100644
index 0000000000..cf8ef5636b
--- /dev/null
+++ b/apps/transport/lib/transport_web/live/feedback_live.ex
@@ -0,0 +1,67 @@
+defmodule TransportWeb.Live.FeedbackLive do
+ use Phoenix.LiveView
+ use TransportWeb.InputHelpers
+ import TransportWeb.InputHelpers
+ import TransportWeb.Gettext
+ require Logger
+
+ @moduledoc """
+ A reusable module to display a feedback form for a given feature, you can display inside a normal view or a live view.
+ In case of normal view, don’t forget to add the app.js script with LiveView js inside the page as if it is not included in the general layout.
+ If you add feedback for a new feature, add it to the list of features.
+ """
+
+ @feedback_rating_values ["like", "neutral", "dislike"]
+ @feedback_features ["gtfs-stops", "on-demand-validation", "gbfs-validation"]
+
+ def mount(_params, %{"feature" => feature, "locale" => locale} = session, socket)
+ when feature in @feedback_features do
+ current_email = session |> get_in(["current_user", "email"])
+
+ socket =
+ socket
+ |> assign(
+ feature: feature,
+ current_email: current_email,
+ feedback_sent: false,
+ feedback_error: false
+ )
+
+ Gettext.put_locale(locale)
+
+ {:ok, socket}
+ end
+
+ def handle_event("submit", %{"feedback" => %{"name" => name, "email" => email}}, socket) when name !== "" do
+ # someone filled the honeypot field ("name") => discard as spam
+ Logger.info("Feedback coming from #{email} has been discarded because it filled the feedback form honeypot")
+ # spammer get a little fox emoji in their flash message, useful for testing purpose
+ {:noreply, socket |> assign(:feedback_sent, true)}
+ end
+
+ def handle_event(
+ "submit",
+ %{"feedback" => %{"rating" => rating, "explanation" => explanation, "email" => email, "feature" => feature}},
+ socket
+ )
+ when rating in @feedback_rating_values and feature in @feedback_features do
+ %{email: email, explanation: explanation} = sanitize_inputs(%{email: email, explanation: explanation})
+
+ feedback_email = TransportWeb.ContactEmail.feedback(rating, explanation, email, feature)
+
+ case Transport.Mailer.deliver(feedback_email) do
+ {:ok, _} ->
+ {:noreply, socket |> assign(:feedback_sent, true)}
+
+ {:error, _} ->
+ {:noreply, socket |> assign(:feedback_error, true)}
+ end
+ end
+
+ def handle_event("submit", session, socket) do
+ Logger.error("Bad parameters for feedback #{inspect(session)}")
+ {:noreply, socket |> assign(:feedback_error, true)}
+ end
+
+ defp sanitize_inputs(map), do: Map.new(map, fn {k, v} -> {k, v |> String.trim() |> HtmlSanitizeEx.strip_tags()} end)
+end
diff --git a/apps/transport/lib/transport_web/live/feedback_live.html.heex b/apps/transport/lib/transport_web/live/feedback_live.html.heex
new file mode 100644
index 0000000000..a29b3ba5b5
--- /dev/null
+++ b/apps/transport/lib/transport_web/live/feedback_live.html.heex
@@ -0,0 +1,59 @@
+
+
<%= dgettext("feedback", "Leave your feedback") %>
+ <.form :let={f} :if={!@feedback_sent} for={%{}} as={:feedback} phx-submit="submit" class="feedback-form no-margin">
+
+
+
+
+
+
+
+
<%= dgettext("feedback", "Thanks for your feedback!") %>
+
<%= gettext("There has been an error, try again later") %>
+
+
+
diff --git a/apps/transport/lib/transport_web/live/on_demand_validation_live.html.heex b/apps/transport/lib/transport_web/live/on_demand_validation_live.html.heex
index 31284b41aa..dc3ceb1ac6 100644
--- a/apps/transport/lib/transport_web/live/on_demand_validation_live.html.heex
+++ b/apps/transport/lib/transport_web/live/on_demand_validation_live.html.heex
@@ -168,5 +168,11 @@
+
+ <%= live_render(@socket, TransportWeb.Live.FeedbackLive,
+ id: "feedback-form",
+ session: %{"feature" => "on-demand-validation"}
+ ) %>
+
diff --git a/apps/transport/lib/transport_web/live/on_demand_validation_select_live.html.heex b/apps/transport/lib/transport_web/live/on_demand_validation_select_live.html.heex
index b1357677fc..23aa63ba84 100644
--- a/apps/transport/lib/transport_web/live/on_demand_validation_select_live.html.heex
+++ b/apps/transport/lib/transport_web/live/on_demand_validation_select_live.html.heex
@@ -63,4 +63,12 @@
+
+
+ <%= live_render(@socket, TransportWeb.Live.FeedbackLive,
+ id: "feedback-form",
+ session: %{"feature" => "on-demand-validation"}
+ ) %>
+
+
diff --git a/apps/transport/lib/transport_web/templates/explore/gtfs_stops.html.heex b/apps/transport/lib/transport_web/templates/explore/gtfs_stops.html.heex
index c0e5783a1b..baf14a33ea 100644
--- a/apps/transport/lib/transport_web/templates/explore/gtfs_stops.html.heex
+++ b/apps/transport/lib/transport_web/templates/explore/gtfs_stops.html.heex
@@ -11,5 +11,12 @@
+
+ <%= live_render(@conn, TransportWeb.Live.FeedbackLive, session: %{"feature" => "gtfs-stops"}) %>
+
+
+
+
diff --git a/apps/transport/lib/transport_web/templates/gbfs_analyzer/index.html.heex b/apps/transport/lib/transport_web/templates/gbfs_analyzer/index.html.heex
index dc7832a868..a5db6dc90a 100644
--- a/apps/transport/lib/transport_web/templates/gbfs_analyzer/index.html.heex
+++ b/apps/transport/lib/transport_web/templates/gbfs_analyzer/index.html.heex
@@ -175,9 +175,16 @@
+
+ <%= live_render(@conn, TransportWeb.Live.FeedbackLive, session: %{"feature" => "gbfs-validation"}) %>
+
+
+
+
diff --git a/apps/transport/lib/transport_web/templates/validation/show.html.heex b/apps/transport/lib/transport_web/templates/validation/show.html.heex
index 22158da60f..5c389c6564 100644
--- a/apps/transport/lib/transport_web/templates/validation/show.html.heex
+++ b/apps/transport/lib/transport_web/templates/validation/show.html.heex
@@ -57,3 +57,10 @@
+
+
+ <%= live_render(@conn, TransportWeb.Live.FeedbackLive, session: %{"feature" => "on-demand-validation"}) %>
+
+
+
diff --git a/apps/transport/priv/gettext/en/LC_MESSAGES/feedback.po b/apps/transport/priv/gettext/en/LC_MESSAGES/feedback.po
new file mode 100644
index 0000000000..4924c9cc2f
--- /dev/null
+++ b/apps/transport/priv/gettext/en/LC_MESSAGES/feedback.po
@@ -0,0 +1,36 @@
+## "msgid"s in this file come from POT (.pot) files.
+###
+### Do not add, change, or remove "msgid"s manually here as
+### they're tied to the ones in the corresponding POT file
+### (with the same domain).
+###
+### Use "mix gettext.extract --merge" or "mix gettext.merge"
+### to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#, elixir-autogen, elixir-format
+msgid "Send the feedback"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Why?"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Your email (optional)"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "What's your feedback on this page?"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Leave your feedback"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Thanks for your feedback!"
+msgstr ""
diff --git a/apps/transport/priv/gettext/feedback.pot b/apps/transport/priv/gettext/feedback.pot
new file mode 100644
index 0000000000..24b75cf023
--- /dev/null
+++ b/apps/transport/priv/gettext/feedback.pot
@@ -0,0 +1,36 @@
+## This file is a PO Template file.
+##
+## "msgid"s here are often extracted from source code.
+## Add new messages manually only if they're dynamic
+## messages that can't be statically extracted.
+##
+## Run "mix gettext.extract" to bring this file up to
+## date. Leave "msgstr"s empty as changing them here has no
+## effect: edit them in PO (.po) files instead.
+#
+msgid ""
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Send the feedback"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Why?"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Your email (optional)"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "What's your feedback on this page?"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Leave your feedback"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "Thanks for your feedback!"
+msgstr ""
diff --git a/apps/transport/priv/gettext/fr/LC_MESSAGES/feedback.po b/apps/transport/priv/gettext/fr/LC_MESSAGES/feedback.po
new file mode 100644
index 0000000000..d091584fb6
--- /dev/null
+++ b/apps/transport/priv/gettext/fr/LC_MESSAGES/feedback.po
@@ -0,0 +1,36 @@
+## "msgid"s in this file come from POT (.pot) files.
+###
+### Do not add, change, or remove "msgid"s manually here as
+### they're tied to the ones in the corresponding POT file
+### (with the same domain).
+###
+### Use "mix gettext.extract --merge" or "mix gettext.merge"
+### to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n>1);\n"
+
+#, elixir-autogen, elixir-format
+msgid "Send the feedback"
+msgstr "Envoyer l’avis"
+
+#, elixir-autogen, elixir-format
+msgid "Why?"
+msgstr "Pourquoi ?"
+
+#, elixir-autogen, elixir-format
+msgid "Your email (optional)"
+msgstr "Votre adresse e-mail (facultatif)"
+
+#, elixir-autogen, elixir-format
+msgid "What's your feedback on this page?"
+msgstr "Qu’avez-vous pensé de cette page ?"
+
+#, elixir-autogen, elixir-format
+msgid "Leave your feedback"
+msgstr "Laissez-nous votre avis"
+
+#, elixir-autogen, elixir-format
+msgid "Thanks for your feedback!"
+msgstr "Merci d’avoir laissé votre avis !"
diff --git a/apps/transport/test/transport_web/controllers/contact_controller_test.exs b/apps/transport/test/transport_web/controllers/contact_controller_test.exs
index 829a71105e..5f3007ecc3 100644
--- a/apps/transport/test/transport_web/controllers/contact_controller_test.exs
+++ b/apps/transport/test/transport_web/controllers/contact_controller_test.exs
@@ -1,8 +1,6 @@
defmodule TransportWeb.ContactControllerTest do
use TransportWeb.ConnCase, async: true
- import Mox
import Swoosh.TestAssertions
- setup :verify_on_exit!
test "Post contact form with honey pot filled", %{conn: conn} do
conn
@@ -21,10 +19,8 @@ defmodule TransportWeb.ContactControllerTest do
contact_path(conn, :send_mail, %{email: "human@user.fr", topic: "dataset", question: "where is my dataset?"})
)
|> get_flash(:info)
- |> case do
- nil -> assert true
- msg -> refute msg =~ "🦊"
- end
+ |> Kernel.=~("🦊")
+ |> refute
assert_email_sent(
from: {"PAN, Formulaire Contact", "contact@transport.beta.gouv.fr"},
diff --git a/apps/transport/test/transport_web/controllers/explore_controller_test.exs b/apps/transport/test/transport_web/controllers/explore_controller_test.exs
index 32c8e06c26..4d816e7fd5 100644
--- a/apps/transport/test/transport_web/controllers/explore_controller_test.exs
+++ b/apps/transport/test/transport_web/controllers/explore_controller_test.exs
@@ -48,9 +48,8 @@ defmodule TransportWeb.ExploreControllerTest do
doc = Floki.parse_document!(html)
[{"title", _, [title]}] = Floki.find(doc, "title")
- [{"h2", _, [h2]}] = Floki.find(doc, "h2")
assert title == "Carte consolidée des arrêts GTFS (beta)"
- assert h2 == "Carte consolidée des arrêts GTFS (beta)"
+ assert html =~ "Carte consolidée des arrêts GTFS (beta)
"
end
end
diff --git a/apps/transport/test/transport_web/live_views/feedback_live_test.exs b/apps/transport/test/transport_web/live_views/feedback_live_test.exs
new file mode 100644
index 0000000000..3f47d977ec
--- /dev/null
+++ b/apps/transport/test/transport_web/live_views/feedback_live_test.exs
@@ -0,0 +1,91 @@
+defmodule TransportWeb.FeedbackLiveTest do
+ use TransportWeb.ConnCase, async: true
+ import Phoenix.LiveViewTest
+ import Swoosh.TestAssertions
+ import ExUnit.CaptureLog
+ import Mox
+
+ setup :verify_on_exit!
+
+ @endpoint TransportWeb.Endpoint
+
+ test "Render the feedback component", %{conn: conn} do
+ {:ok, _view, html} =
+ live_isolated(conn, TransportWeb.Live.FeedbackLive,
+ session: %{"feature" => "on-demand-validation", "locale" => "fr"}
+ )
+
+ assert html =~ "Qu’avez-vous pensé de cette page ?"
+ end
+
+ test "Post feedback form with honey pot filled", %{conn: conn} do
+ {:ok, view, _html} =
+ live_isolated(conn, TransportWeb.Live.FeedbackLive,
+ session: %{"feature" => "on-demand-validation", "locale" => "fr"}
+ )
+
+ view
+ |> element("form")
+ |> render_submit(%{feedback: %{email: "spammer@internet.com", name: "John Doe"}})
+ |> Kernel.=~("Merci d’avoir laissé votre avis !")
+ |> assert
+
+ assert_no_email_sent()
+ end
+
+ test "Post feedback form without honey pot", %{conn: conn} do
+ {:ok, view, _html} =
+ live_isolated(conn, TransportWeb.Live.FeedbackLive,
+ session: %{"feature" => "on-demand-validation", "locale" => "fr"}
+ )
+
+ view
+ |> element("form")
+ |> render_submit(%{
+ feedback: %{
+ email: "",
+ feature: "on-demand-validation",
+ rating: "like",
+ explanation: "so useful for my GTFS files"
+ }
+ })
+ |> Kernel.=~("Merci d’avoir laissé votre avis !")
+ |> assert
+
+ assert_email_sent(
+ from: {"Formulaire feedback", "contact@transport.beta.gouv.fr"},
+ to: "contact@transport.beta.gouv.fr",
+ subject: "Nouvel avis pour on-demand-validation : j’aime",
+ text_body:
+ "Vous avez un nouvel avis sur le PAN.\nFonctionnalité : on-demand-validation\nNotation : j’aime\nAdresse e-mail : \n\nExplication : so useful for my GTFS files\n",
+ html_body: nil,
+ reply_to: "contact@transport.beta.gouv.fr"
+ )
+ end
+
+ test "Post invalid parameters in feedback form and check it doesn’t crash", %{conn: conn} do
+ {:ok, view, _html} =
+ live_isolated(conn, TransportWeb.Live.FeedbackLive,
+ session: %{"feature" => "on-demand-validation", "locale" => "fr"}
+ )
+
+ {view, logs} =
+ with_log(fn ->
+ view
+ |> element("form")
+ |> render_submit(%{topic: "question", demande: "where is my dataset?"})
+ end)
+
+ assert view =~ "Il y a eu une erreur réessayez."
+
+ assert logs =~ "Bad parameters for feedback"
+
+ assert_no_email_sent()
+ end
+
+ test "Is correctly included in the validation Liveview", %{conn: conn} do
+ Transport.Shared.Schemas.Mock |> expect(:transport_schemas, 2, fn -> %{} end)
+ {:ok, _view, html} = conn |> live(live_path(conn, TransportWeb.Live.OnDemandValidationSelectLive))
+ assert html =~ "Laissez-nous votre avis
"
+ end
+end