diff --git a/lib/mix/tasks/send_email_reports.ex b/lib/mix/tasks/send_email_reports.ex index 02d1b0066..660e67157 100644 --- a/lib/mix/tasks/send_email_reports.ex +++ b/lib/mix/tasks/send_email_reports.ex @@ -2,89 +2,98 @@ defmodule Mix.Tasks.SendEmailReports do use Mix.Task use Plausible.Repo require Logger + alias Plausible.Stats - def run(args) do + def run(_args) do Application.ensure_all_started(:plausible) - execute(args) + execute(Timex.now()) end @doc""" The email report should be sent on Monday at 9am according to the timezone of the site. This job runs every hour to be able to send it with hourly precision. """ - def execute(_args \\ []) do - send_weekly_emails() - send_monthly_emails() + def execute(job_start) do + send_weekly_emails(job_start) + send_monthly_emails(job_start) end - defp send_weekly_emails() do + defp send_weekly_emails(job_start) do sites = Repo.all( from s in Plausible.Site, join: wr in Plausible.Site.WeeklyReport, on: wr.site_id == s.id, - left_join: se in "sent_weekly_reports", on: se.site_id == s.id and se.year == fragment("EXTRACT(isoyear from (now() at time zone ?))", s.timezone) and se.week == fragment("EXTRACT(week from (now() at time zone ?))", s.timezone), + left_join: se in "sent_weekly_reports", on: se.site_id == s.id and se.year == fragment("EXTRACT(isoyear from (? at time zone ?))", ^job_start, s.timezone) and se.week == fragment("EXTRACT(week from (? at time zone ?))", ^job_start, s.timezone), where: is_nil(se), # We haven't sent a report for this site on this week - where: fragment("EXTRACT(dow from (now() at time zone ?))", s.timezone) == 1, # It's monday in the local timezone - where: fragment("EXTRACT(hour from (now() at time zone ?))", s.timezone) >= 9, # It's after 9am + where: fragment("EXTRACT(dow from (? at time zone ?))", ^job_start, s.timezone) == 1, # It's monday in the local timezone + where: fragment("EXTRACT(hour from (? at time zone ?))", ^job_start, s.timezone) >= 9, # It's after 9am preload: [weekly_report: wr] ) for site <- sites do - email = site.weekly_report.email - query = Plausible.Stats.Query.from(site.timezone, %{"period" => "7d"}) + query = Stats.Query.from(site.timezone, %{"period" => "7d"}) - IO.puts("Sending weekly report for #{site.domain} to #{email}") + for email <- site.weekly_report.recipients do + Logger.info("Sending weekly report for #{site.domain} to #{email}") + unsubscribe_link = PlausibleWeb.Endpoint.url() <> "/sites/#{site.domain}/weekly-report/unsubscribe?email=#{email}" + send_report(email, site, "Weekly", unsubscribe_link, query) + end - send_report(email, site, query) - weekly_report_sent(site) + weekly_report_sent(site, job_start) end end - defp send_monthly_emails() do + defp send_monthly_emails(job_start) do sites = Repo.all( from s in Plausible.Site, join: mr in Plausible.Site.MonthlyReport, on: mr.site_id == s.id, - left_join: se in "sent_monthly_reports", on: se.site_id == s.id and se.year == fragment("EXTRACT(year from (now() at time zone ?))", s.timezone) and se.month == fragment("EXTRACT(month from (now() at time zone ?))", s.timezone), + left_join: se in "sent_monthly_reports", on: se.site_id == s.id and se.year == fragment("EXTRACT(year from (? at time zone ?))", ^job_start, s.timezone) and se.month == fragment("EXTRACT(month from (? at time zone ?))", ^job_start, s.timezone), where: is_nil(se), # We haven't sent a report for this site this month - where: fragment("EXTRACT(day from (now() at time zone ?))", s.timezone) == 1, # It's the 1st of the month in the local timezone - where: fragment("EXTRACT(hour from (now() at time zone ?))", s.timezone) >= 9, # It's after 9am + where: fragment("EXTRACT(day from (? at time zone ?))", ^job_start, s.timezone) == 1, # It's the 1st of the month in the local timezone + where: fragment("EXTRACT(hour from (? at time zone ?))", ^job_start, s.timezone) >= 9, # It's after 9am preload: [monthly_report: mr] ) for site <- sites do - email = site.monthly_report.email - last_month = Timex.now(site.timezone) |> Timex.shift(months: -1) |> Timex.beginning_of_month |> Timex.format!("{ISOdate}") - query = Plausible.Stats.Query.from(site.timezone, %{"period" => "month", "date" => last_month}) + last_month = job_start |> Timex.Timezone.convert(site.timezone) |> Timex.shift(months: -1) |> Timex.beginning_of_month + query = Stats.Query.from(site.timezone, %{"period" => "month", "date" => Timex.format!(last_month, "{ISOdate}")}) - IO.puts("Sending monthly report for #{site.domain} to #{email}") + for email <- site.monthly_report.recipients do + Logger.info("Sending monthly report for #{site.domain} to #{email}") + unsubscribe_link = PlausibleWeb.Endpoint.url() <> "/sites/#{site.domain}/monthly-report/unsubscribe?email=#{email}" + send_report(email, site, Timex.format!(last_month, "{Mfull}"), unsubscribe_link, query) + end - send_report(email, site, query) - monthly_report_sent(site) + monthly_report_sent(site, job_start) end end - defp send_report(email, site, query) do - {pageviews, unique_visitors} = Plausible.Stats.pageviews_and_visitors(site, query) - {change_pageviews, change_visitors} = Plausible.Stats.compare_pageviews_and_visitors(site, query, {pageviews, unique_visitors}) - referrers = Plausible.Stats.top_referrers(site, query) - pages = Plausible.Stats.top_pages(site, query) - settings_link = PlausibleWeb.Endpoint.url() <> "/#{site.domain}/settings#email-reports" - view_link = PlausibleWeb.Endpoint.url() <> "/#{site.domain}?period=7d" + defp send_report(email, site, name, unsubscribe_link, query) do + {pageviews, unique_visitors} = Stats.pageviews_and_visitors(site, query) + {change_pageviews, change_visitors} = Stats.compare_pageviews_and_visitors(site, query, {pageviews, unique_visitors}) + bounce_rate = Stats.bounce_rate(site, query) + prev_bounce_rate = Stats.bounce_rate(site, Stats.Query.shift_back(query)) + change_bounce_rate = if prev_bounce_rate > 0, do: bounce_rate - prev_bounce_rate + referrers = Stats.top_referrers(site, query) + pages = Stats.top_pages(site, query) PlausibleWeb.Email.weekly_report(email, site, unique_visitors: unique_visitors, change_visitors: change_visitors, pageviews: pageviews, change_pageviews: change_pageviews, + bounce_rate: bounce_rate, + change_bounce_rate: change_bounce_rate, referrers: referrers, - settings_link: settings_link, - view_link: view_link, + unsubscribe_link: unsubscribe_link, + view_link: view_link(site, query), pages: pages, - query: query + query: query, + name: name ) |> Plausible.Mailer.deliver_now() end - defp weekly_report_sent(site) do - {year, week} = Timex.now(site.timezone) |> DateTime.to_date |> Timex.iso_week + defp weekly_report_sent(site, time) do + {year, week} = time |> DateTime.to_date |> Timex.iso_week Repo.insert_all("sent_weekly_reports", [%{ site_id: site.id, @@ -94,8 +103,8 @@ defmodule Mix.Tasks.SendEmailReports do }]) end - defp monthly_report_sent(site) do - date = Timex.now(site.timezone) |> DateTime.to_date + defp monthly_report_sent(site, time) do + date = DateTime.to_date(time) Repo.insert_all("sent_monthly_reports", [%{ site_id: site.id, @@ -104,4 +113,13 @@ defmodule Mix.Tasks.SendEmailReports do timestamp: Timex.now() }]) end + + defp view_link(site, %Plausible.Stats.Query{period: "7d"}) do + PlausibleWeb.Endpoint.url() <> "/#{site.domain}?period=7d" + end + + defp view_link(site, %Plausible.Stats.Query{period: "month", date_range: range}) do + month = Timex.format!(range.first, "{ISOdate}") + PlausibleWeb.Endpoint.url() <> "/#{site.domain}?period=month&date=#{month}" + end end diff --git a/lib/plausible/site/monthly_report.ex b/lib/plausible/site/monthly_report.ex index e98fd91be..e6069ff13 100644 --- a/lib/plausible/site/monthly_report.ex +++ b/lib/plausible/site/monthly_report.ex @@ -1,10 +1,9 @@ defmodule Plausible.Site.MonthlyReport do use Ecto.Schema import Ecto.Changeset - @mail_regex ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/ schema "monthly_reports" do - field :email, :string + field :recipients, {:array, :string} belongs_to :site, Plausible.Site timestamps() @@ -12,9 +11,18 @@ defmodule Plausible.Site.MonthlyReport do def changeset(settings, attrs \\ %{}) do settings - |> cast(attrs, [:site_id, :email]) - |> validate_required([:site_id, :email]) - |> validate_format(:email, @mail_regex) + |> cast(attrs, [:site_id, :recipients]) + |> validate_required([:site_id, :recipients]) |> unique_constraint(:site) end + + def add_recipient(report, recipient) do + report + |> change(recipients: report.recipients ++ [recipient]) + end + + def remove_recipient(report, recipient) do + report + |> change(recipients: List.delete(report.recipients, recipient)) + end end diff --git a/lib/plausible/site/weekly_report.ex b/lib/plausible/site/weekly_report.ex index 941a51875..80f533cfe 100644 --- a/lib/plausible/site/weekly_report.ex +++ b/lib/plausible/site/weekly_report.ex @@ -1,10 +1,9 @@ defmodule Plausible.Site.WeeklyReport do use Ecto.Schema import Ecto.Changeset - @mail_regex ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/ schema "weekly_reports" do - field :email, :string + field :recipients, {:array, :string} belongs_to :site, Plausible.Site timestamps() @@ -12,9 +11,18 @@ defmodule Plausible.Site.WeeklyReport do def changeset(settings, attrs \\ %{}) do settings - |> cast(attrs, [:site_id, :email]) - |> validate_required([:site_id, :email]) - |> validate_format(:email, @mail_regex) + |> cast(attrs, [:site_id, :recipients]) + |> validate_required([:site_id, :recipients]) |> unique_constraint(:site) end + + def add_recipient(report, recipient) do + report + |> change(recipients: report.recipients ++ [recipient]) + end + + def remove_recipient(report, recipient) do + report + |> change(recipients: List.delete(report.recipients, recipient)) + end end diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 32c058716..ec55779e0 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -82,17 +82,15 @@ defmodule PlausibleWeb.SiteController do end weekly_report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - weekly_report_changeset = weekly_report && Plausible.Site.WeeklyReport.changeset(weekly_report, %{}) monthly_report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - monthly_report_changeset = monthly_report && Plausible.Site.WeeklyReport.changeset(monthly_report, %{}) goals = Goals.for_site(site.domain) conn |> assign(:skip_plausible_tracking, true) |> render("settings.html", site: site, - weekly_report_changeset: weekly_report_changeset, - monthly_report_changeset: monthly_report_changeset, + weekly_report: weekly_report, + monthly_report: monthly_report, search_console_domains: search_console_domains, goals: goals, changeset: Plausible.Site.changeset(site, %{}) @@ -172,13 +170,13 @@ defmodule PlausibleWeb.SiteController do Plausible.Site.WeeklyReport.changeset(%Plausible.Site.WeeklyReport{}, %{ site_id: site.id, - email: conn.assigns[:current_user].email + recipients: [conn.assigns[:current_user].email] }) |> Repo.insert! conn |> put_flash(:success, "Success! You will receive an email report every Monday going forward") - |> redirect(to: "/" <> site.domain <> "/settings") + |> redirect(to: "/" <> site.domain <> "/settings#email-reports") end def disable_weekly_report(conn, %{"website" => website}) do @@ -187,18 +185,31 @@ defmodule PlausibleWeb.SiteController do conn |> put_flash(:success, "Success! You will not receive weekly email reports going forward") - |> redirect(to: "/" <> site.domain <> "/settings") + |> redirect(to: "/" <> site.domain <> "/settings#email-reports") end - def update_weekly_settings(conn, %{"website" => website, "weekly_report" => weekly_report}) do + def add_weekly_report_recipient(conn, %{"website" => website, "recipient" => recipient}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - |> Plausible.Site.WeeklyReport.changeset(weekly_report) + |> Plausible.Site.WeeklyReport.add_recipient(recipient) |> Repo.update! conn - |> put_flash(:success, "Email address saved succesfully") - |> redirect(to: "/#{site.domain}/settings") + |> put_flash(:success, "Succesfully added #{recipient} as a recipient for the weekly report") + |> redirect(to: "/#{site.domain}/settings#email-reports") + end + + def remove_weekly_report_recipient(conn, %{"website" => website, "recipient" => recipient}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + + Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + |> Plausible.Site.WeeklyReport.remove_recipient(recipient) + |> Repo.update! + + conn + |> put_flash(:success, "Succesfully removed #{recipient} as a recipient for the weekly report") + |> redirect(to: "/#{site.domain}/settings#email-reports") end def enable_monthly_report(conn, %{"website" => website}) do @@ -206,13 +217,13 @@ defmodule PlausibleWeb.SiteController do Plausible.Site.MonthlyReport.changeset(%Plausible.Site.MonthlyReport{}, %{ site_id: site.id, - email: conn.assigns[:current_user].email + recipients: [conn.assigns[:current_user].email] }) |> Repo.insert! conn |> put_flash(:success, "Success! You will receive an email report every month going forward") - |> redirect(to: "/" <> site.domain <> "/settings") + |> redirect(to: "/" <> site.domain <> "/settings#email-reports") end def disable_monthly_report(conn, %{"website" => website}) do @@ -221,32 +232,31 @@ defmodule PlausibleWeb.SiteController do conn |> put_flash(:success, "Success! You will not receive monthly email reports going forward") - |> redirect(to: "/" <> site.domain <> "/settings") + |> redirect(to: "/" <> site.domain <> "/settings#email-reports") end - def update_monthly_settings(conn, %{"website" => website, "monthly_report" => monthly_report}) do + def add_monthly_report_recipient(conn, %{"website" => website, "recipient" => recipient}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - |> Plausible.Site.WeeklyReport.changeset(monthly_report) + |> Plausible.Site.MonthlyReport.add_recipient(recipient) |> Repo.update! conn - |> put_flash(:success, "Email address saved succesfully") - |> redirect(to: "/#{site.domain}/settings") + |> put_flash(:success, "Succesfully added #{recipient} as a recipient for the monthly report") + |> redirect(to: "/#{site.domain}/settings#email-reports") end - def google_settings(conn, %{"website" => website}) do + def remove_monthly_report_recipient(conn, %{"website" => website, "recipient" => recipient}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) - |> Repo.preload(:google_auth) - verified_domains = Plausible.Google.Api.fetch_verified_properties(site.google_auth) + Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) + |> Plausible.Site.MonthlyReport.remove_recipient(recipient) + |> Repo.update! - render(conn, - "google_settings.html", - site: site, - verified_domains: verified_domains, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) + conn + |> put_flash(:success, "Succesfully removed #{recipient} as a recipient for the monthly report") + |> redirect(to: "/#{site.domain}/settings#email-reports") end defp insert_site(user_id, params) do diff --git a/lib/plausible_web/controllers/unsubscribe_controller.ex b/lib/plausible_web/controllers/unsubscribe_controller.ex new file mode 100644 index 000000000..92e211d2e --- /dev/null +++ b/lib/plausible_web/controllers/unsubscribe_controller.ex @@ -0,0 +1,25 @@ +defmodule PlausibleWeb.UnsubscribeController do + use PlausibleWeb, :controller + use Plausible.Repo + alias Plausible.Site.{WeeklyReport, MonthlyReport} + + def weekly_report(conn, %{"website" => website, "email" => email}) do + site = Repo.get_by(Plausible.Site, domain: website) + + Repo.get_by(WeeklyReport, site_id: site.id) + |> WeeklyReport.remove_recipient(email) + |> Repo.update! + + render(conn, "success.html", interval: "weekly", site: website, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def monthly_report(conn, %{"website" => website, "email" => email}) do + site = Repo.get_by(Plausible.Site, domain: website) + + Repo.get_by(MonthlyReport, site_id: site.id) + |> MonthlyReport.remove_recipient(email) + |> Repo.update! + + render(conn, "success.html", interval: "monthly", site: website, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end +end diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex index 3bbf975c8..b7c3baf61 100644 --- a/lib/plausible_web/email.ex +++ b/lib/plausible_web/email.ex @@ -91,7 +91,7 @@ defmodule PlausibleWeb.Email do |> to(email) |> from("Plausible Insights ") |> tag("weekly-report") - |> subject("Weekly report for #{site.domain}") + |> subject("#{assigns[:name]} report for #{site.domain}") |> render("weekly_report.html", Keyword.put(assigns, :site, site)) end end diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 1d8ff095d..3d5806d81 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -102,10 +102,16 @@ defmodule PlausibleWeb.Router do post "/sites/:website/make-private", SiteController, :make_private post "/sites/:website/weekly-report/enable", SiteController, :enable_weekly_report post "/sites/:website/weekly-report/disable", SiteController, :disable_weekly_report - put "/sites/:website/weekly-report", SiteController, :update_weekly_settings + post "/sites/:website/weekly-report/recipients", SiteController, :add_weekly_report_recipient + delete "/sites/:website/weekly-report/recipients/:recipient", SiteController, :remove_weekly_report_recipient post "/sites/:website/monthly-report/enable", SiteController, :enable_monthly_report post "/sites/:website/monthly-report/disable", SiteController, :disable_monthly_report - put "/sites/:website/monthly-report", SiteController, :update_monthly_settings + post "/sites/:website/monthly-report/recipients", SiteController, :add_monthly_report_recipient + delete "/sites/:website/monthly-report/recipients/:recipient", SiteController, :remove_monthly_report_recipient + + get "/sites/:website/weekly-report/unsubscribe", UnsubscribeController, :weekly_report + get "/sites/:website/monthly-report/unsubscribe", UnsubscribeController, :monthly_report + get "/:website/snippet", SiteController, :add_snippet get "/:website/settings", SiteController, :settings get "/:website/goals", SiteController, :goals diff --git a/lib/plausible_web/templates/email/weekly_report.html.eex b/lib/plausible_web/templates/email/weekly_report.html.eex index cd55d6df1..d9dfdb122 100644 --- a/lib/plausible_web/templates/email/weekly_report.html.eex +++ b/lib/plausible_web/templates/email/weekly_report.html.eex @@ -149,7 +149,7 @@ body {
-

Weekly report for <%= @site.domain %>

+

<%= @name %> report for <%= @site.domain %>

@@ -299,14 +299,43 @@ body {
-
+ +
+
BOUNCE RATE
+
+ + +
+
+

<%= @bounce_rate %>%

+
+
+ + +
+ <%= cond do %> + <% @change_bounce_rate == nil -> %> +
+

N/A

+
+ <% @change_bounce_rate <= 0 -> %> +
+

<%= @change_bounce_rate %>%

+
+ <% @change_bounce_rate > 0 -> %> +
+

+<%= @change_bounce_rate %>%

+
+ <% end %> +
+
- + @@ -580,7 +609,7 @@ body {
- View last 7 days on Plausible + View on Plausible
@@ -642,7 +671,7 @@ body {
-

You are receiving this email because you have enabled weekly email reports for <%= link(@site.domain, rel: "nofollow", style: "text-decoration: none; color: #3d4852", to: "#") %>. Click here to manage your notification settings.

+

Don't want to receive these emails? Click here to unsubscribe.

diff --git a/lib/plausible_web/templates/layout/_svg_icons.html.eex b/lib/plausible_web/templates/layout/_svg_icons.html.eex index 83aae7b0e..51cbfbe86 100644 --- a/lib/plausible_web/templates/layout/_svg_icons.html.eex +++ b/lib/plausible_web/templates/layout/_svg_icons.html.eex @@ -23,5 +23,7 @@ + + diff --git a/lib/plausible_web/templates/site/google_settings.html.eex b/lib/plausible_web/templates/site/google_settings.html.eex deleted file mode 100644 index b326e7936..000000000 --- a/lib/plausible_web/templates/site/google_settings.html.eex +++ /dev/null @@ -1,16 +0,0 @@ -<%= form_for @conn, "/google", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

Connect to property

-

Select the Google Search Console property you would like to pull keyword data from

- -
- <%= label f, :domain, class: "block text-grey-darker text-sm font-bold mb-2" %> -
- <%= select f, :domain, @verified_domains, class: "block appearance-none w-full bg-grey-lighter text-grey-darker cursor-pointer hover:border-grey p-2 pr-8 rounded leading-normal focus:outline-none" %> -
- -
-
-
- - <%= submit "Enable Integration →", class: "button mt-4 w-full" %> -<% end %> diff --git a/lib/plausible_web/templates/site/settings.html.eex b/lib/plausible_web/templates/site/settings.html.eex index fd81f9112..67b6ec31b 100644 --- a/lib/plausible_web/templates/site/settings.html.eex +++ b/lib/plausible_web/templates/site/settings.html.eex @@ -115,14 +115,12 @@
-
-

Email reports

-
+

Email reports

- <%= if @weekly_report_changeset do %> + <%= if @weekly_report do %> <%= button(to: "/sites/#{@site.domain}/weekly-report/disable", method: :post, class: "border rounded-full border-grey flex items-center cursor-pointer w-8 bg-green justify-end") do %> <% end %> @@ -131,22 +129,28 @@ <% end %> <% end %> - Receive a weekly email report every Monday + Send a weekly email report every Monday
- <%= if @weekly_report_changeset do %> + <%= if @weekly_report do %>
- <%= form_for @weekly_report_changeset, "/sites/#{@site.domain}/weekly-report", [class: "max-w-xs"], fn f -> %> - <%= label f, :email, "Email address", class: "block text-grey-darker text-sm font-bold mb-2" %> -
- <%= email_input f, :email, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:border-grey-light", style: "flex-grow: 2" %> - <%= submit "Update", class: "button rounded-l-none" %> +

Weekly report recipients

+ <%= for recipient <- @weekly_report.recipients do %> +
+ <%= recipient %> + <%= button("❌", to: "/sites/#{@site.domain}/weekly-report/recipients/#{recipient}", method: :delete) %> +
+ <% end %> + <%= form_for @conn, "/sites/#{@site.domain}/weekly-report/recipients", fn f -> %> +
+ <%= email_input f, :recipient, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:border-grey-light", style: "flex-grow: 2", placeholder: "recipient@example.com" %> + <%= submit "Add recipient", class: "button rounded-l-none whitespace-no-wrap" %>
<% end %>
<% end %> -
+
- <%= if @monthly_report_changeset do %> + <%= if @monthly_report do %> <%= button(to: "/sites/#{@site.domain}/monthly-report/disable", method: :post, class: "border rounded-full border-grey flex items-center cursor-pointer w-8 bg-green justify-end") do %> <% end %> @@ -155,15 +159,21 @@ <% end %> <% end %> - Receive a monthly email report on 1st of the month + Send a monthly email report on 1st of the month
- <%= if @monthly_report_changeset do %> + <%= if @monthly_report do %>
- <%= form_for @monthly_report_changeset, "/sites/#{@site.domain}/monthly-report", [class: "max-w-xs"], fn f -> %> - <%= label f, :email, "Email address", class: "block text-grey-darker text-sm font-bold mb-2" %> -
- <%= email_input f, :email, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:border-grey-light", style: "flex-grow: 2" %> - <%= submit "Update", class: "button rounded-l-none" %> +

Monthly report recipients

+ <%= for recipient <- @monthly_report.recipients do %> +
+ <%= recipient %> + <%= button("❌", to: "/sites/#{@site.domain}/monthly-report/recipients/#{recipient}", method: :delete) %> +
+ <% end %> + <%= form_for @conn, "/sites/#{@site.domain}/monthly-report/recipients", fn f -> %> +
+ <%= email_input f, :recipient, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:border-grey-light", style: "flex-grow: 2", placeholder: "recipient@example.com" %> + <%= submit "Add recipient", class: "button rounded-l-none whitespace-no-wrap" %>
<% end %>
diff --git a/lib/plausible_web/templates/unsubscribe/success.html.eex b/lib/plausible_web/templates/unsubscribe/success.html.eex new file mode 100644 index 000000000..eb37c3308 --- /dev/null +++ b/lib/plausible_web/templates/unsubscribe/success.html.eex @@ -0,0 +1,4 @@ +
+

Unsubscribe successful

+

You will no longer receive a <%= @interval %> analytics report for <%= @site %>

+
diff --git a/lib/plausible_web/views/unsubscribe_view.ex b/lib/plausible_web/views/unsubscribe_view.ex new file mode 100644 index 000000000..ecb1b20ed --- /dev/null +++ b/lib/plausible_web/views/unsubscribe_view.ex @@ -0,0 +1,3 @@ +defmodule PlausibleWeb.UnsubscribeView do + use PlausibleWeb, :view +end diff --git a/priv/repo/migrations/20200121091251_add_recipients.exs b/priv/repo/migrations/20200121091251_add_recipients.exs new file mode 100644 index 000000000..d7fa4c024 --- /dev/null +++ b/priv/repo/migrations/20200121091251_add_recipients.exs @@ -0,0 +1,28 @@ +defmodule Plausible.Repo.Migrations.AddRecipients do + use Ecto.Migration + + def up do + alter table(:weekly_reports) do + add :recipients, {:array, :citext}, null: false, default: [] + end + + execute "UPDATE weekly_reports SET recipients = array_append(recipients, email)" + + alter table(:weekly_reports) do + remove :email + end + + alter table(:monthly_reports) do + add :recipients, {:array, :citext}, null: false, default: [] + end + + execute "UPDATE monthly_reports SET recipients = array_append(recipients, email)" + + alter table(:monthly_reports) do + remove :email + end + end + + def down do + end +end diff --git a/test/mix/tasks/send_email_reports_test.exs b/test/mix/tasks/send_email_reports_test.exs new file mode 100644 index 000000000..f8432acfc --- /dev/null +++ b/test/mix/tasks/send_email_reports_test.exs @@ -0,0 +1,95 @@ +defmodule Mix.Tasks.EmailReportsTest do + use Plausible.DataCase + use Bamboo.Test + alias Mix.Tasks.SendEmailReports + + describe "weekly reports" do + test "sends weekly report on Monday 9am local timezone" do + site = insert(:site, timezone: "US/Eastern") + insert(:weekly_report, site: site, recipients: ["user@email.com"]) + time = Timex.now() |> Timex.beginning_of_week |> Timex.shift(hours: 14) # 2pm UTC is 10am EST + + SendEmailReports.execute(time) + + assert_email_delivered_with(subject: "Weekly report for #{site.domain}", to: [nil: "user@email.com"]) + end + + test "does not send a report on Monday before 9am in local timezone" do + site = insert(:site, timezone: "US/Eastern") + insert(:weekly_report, site: site, recipients: ["user@email.com"]) + time = Timex.now() |> Timex.beginning_of_week |> Timex.shift(hours: 12) # 12pm UTC is 8am EST + + SendEmailReports.execute(time) + + assert_no_emails_delivered() + end + + test "does not send a report on Tuesday" do + site = insert(:site) + insert(:weekly_report, site: site, recipients: ["user@email.com"]) + time = Timex.now() |> Timex.beginning_of_week |> Timex.shift(days: 1, hours: 10) + + SendEmailReports.execute(time) + + assert_no_emails_delivered() + end + + test "does not send the same report multiple times on the same week" do + site = insert(:site) + insert(:weekly_report, site: site, recipients: ["user@email.com"]) + time = Timex.now() |> Timex.beginning_of_week |> Timex.shift(hours: 10) + + SendEmailReports.execute(time) + + assert_email_delivered_with(subject: "Weekly report for #{site.domain}", to: [nil: "user@email.com"]) + + SendEmailReports.execute(time) + assert_no_emails_delivered() + end + end + + describe "monthly_reports" do + test "sends monthly report on the 1st of the month after 9am local timezone" do + site = insert(:site, timezone: "US/Eastern") + insert(:monthly_report, site: site, recipients: ["user@email.com"]) + {:ok, time, _} = DateTime.from_iso8601("2019-04-01T14:00:00Z") + + SendEmailReports.execute(time) + + assert_email_delivered_with(subject: "March report for #{site.domain}", to: [nil: "user@email.com"]) + end + + test "does not send a report on the 1st of the month before 9am in local timezone" do + site = insert(:site, timezone: "US/Eastern") + insert(:monthly_report, site: site, recipients: ["user@email.com"]) + time = Timex.now() |> Timex.beginning_of_month |> Timex.shift(hours: 12) # 12pm UTC is 8am EST + + SendEmailReports.execute(time) + + assert_no_emails_delivered() + end + + test "does not send a report on the 2nd of the month" do + site = insert(:site) + insert(:monthly_report, site: site, recipients: ["user@email.com"]) + time = Timex.now() |> Timex.beginning_of_month |> Timex.shift(days: 1, hours: 10) + + SendEmailReports.execute(time) + + assert_no_emails_delivered() + end + + test "does not send the same report multiple times on the same month" do + site = insert(:site) + insert(:monthly_report, site: site, recipients: ["user@email.com"]) + {:ok, time, _} = DateTime.from_iso8601("2019-02-01T11:00:00Z") + + SendEmailReports.execute(time) + + assert_email_delivered_with(subject: "January report for #{site.domain}", to: [nil: "user@email.com"]) + + SendEmailReports.execute(time) + assert_no_emails_delivered() + end + end +end diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index 1de730417..8b72569cf 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -187,4 +187,102 @@ defmodule PlausibleWeb.SiteControllerTest do assert Repo.aggregate(Plausible.Goal, :count, :id) == 0 end end + + describe "POST /sites/:website/weekly-report/enable" do + setup [:create_user, :log_in, :create_site] + + test "creates a weekly report record with the user email", %{conn: conn, site: site, user: user} do + post(conn, "/sites/#{site.domain}/weekly-report/enable") + + report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + assert report.recipients == [user.email] + end + end + + describe "POST /sites/:website/weekly-report/disable" do + setup [:create_user, :log_in, :create_site] + + test "deletes the weekly report record", %{conn: conn, site: site} do + insert(:weekly_report, site: site) + + post(conn, "/sites/#{site.domain}/weekly-report/disable") + + refute Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + end + end + + describe "POST /sites/:website/weekly-report/recipients" do + setup [:create_user, :log_in, :create_site] + + test "adds a recipient to the weekly report", %{conn: conn, site: site} do + insert(:weekly_report, site: site) + + post(conn, "/sites/#{site.domain}/weekly-report/recipients", recipient: "user@email.com") + + report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + assert report.recipients == ["user@email.com"] + end + end + + describe "DELETE /sites/:website/weekly-report/recipients/:recipient" do + setup [:create_user, :log_in, :create_site] + + test "removes a recipient from the weekly report", %{conn: conn, site: site} do + insert(:weekly_report, site: site, recipients: ["recipient@email.com"]) + + delete(conn, "/sites/#{site.domain}/weekly-report/recipients/recipient@email.com") + + report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + assert report.recipients == [] + end + end + + describe "POST /sites/:website/monthly-report/enable" do + setup [:create_user, :log_in, :create_site] + + test "creates a monthly report record with the user email", %{conn: conn, site: site, user: user} do + post(conn, "/sites/#{site.domain}/monthly-report/enable") + + report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) + assert report.recipients == [user.email] + end + end + + describe "POST /sites/:website/monthly-report/disable" do + setup [:create_user, :log_in, :create_site] + + test "deletes the monthly report record", %{conn: conn, site: site} do + insert(:monthly_report, site: site) + + post(conn, "/sites/#{site.domain}/monthly-report/disable") + + refute Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) + end + end + + describe "POST /sites/:website/monthly-report/recipients" do + setup [:create_user, :log_in, :create_site] + + test "adds a recipient to the monthly report", %{conn: conn, site: site} do + insert(:monthly_report, site: site) + + post(conn, "/sites/#{site.domain}/monthly-report/recipients", recipient: "user@email.com") + + report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) + assert report.recipients == ["user@email.com"] + end + end + + describe "DELETE /sites/:website/monthly-report/recipients/:recipient" do + setup [:create_user, :log_in, :create_site] + + test "removes a recipient from the monthly report", %{conn: conn, site: site} do + insert(:monthly_report, site: site, recipients: ["recipient@email.com"]) + + delete(conn, "/sites/#{site.domain}/monthly-report/recipients/recipient@email.com") + + report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) + assert report.recipients == [] + end + end end diff --git a/test/plausible_web/controllers/unsubscribe_controller_test.exs b/test/plausible_web/controllers/unsubscribe_controller_test.exs new file mode 100644 index 000000000..828e462be --- /dev/null +++ b/test/plausible_web/controllers/unsubscribe_controller_test.exs @@ -0,0 +1,32 @@ +defmodule PlausibleWeb.UnsubscribeControllerTest do + use PlausibleWeb.ConnCase + use Plausible.Repo + + describe "GET /sites/:website/weekly-report/unsubscribe" do + test "removes a recipient from the weekly report without them having to log in", %{conn: conn} do + site = insert(:site) + insert(:weekly_report, site: site, recipients: ["recipient@email.com"]) + + conn = get(conn, "/sites/#{site.domain}/weekly-report/unsubscribe?email=recipient@email.com") + + assert html_response(conn, 200) =~ "Unsubscribe successful" + + report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + assert report.recipients == [] + end + end + + describe "GET /sites/:website/monthly-report/unsubscribe" do + test "removes a recipient from the weekly report without them having to log in", %{conn: conn} do + site = insert(:site) + insert(:monthly_report, site: site, recipients: ["recipient@email.com"]) + + conn = get(conn, "/sites/#{site.domain}/monthly-report/unsubscribe?email=recipient@email.com") + + assert html_response(conn, 200) =~ "Unsubscribe successful" + + report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) + assert report.recipients == [] + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index ac9529155..8519f5adc 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -50,8 +50,7 @@ defmodule Plausible.Factory do %Plausible.Event{ hostname: hostname, pathname: "/", - new_visitor: true, - user_id: UUID.uuid4(), + new_visitor: true, user_id: UUID.uuid4(), } end @@ -90,4 +89,12 @@ defmodule Plausible.Factory do created: Timex.now() } end + + def weekly_report_factory do + %Plausible.Site.WeeklyReport{} + end + + def monthly_report_factory do + %Plausible.Site.MonthlyReport{} + end end