From 17e4dc96ef917c7c6b897a0c2d233e68101f3917 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Mon, 9 Sep 2019 12:19:21 +0100 Subject: [PATCH 1/3] Rename email settings to weekly report --- lib/mix/tasks/send_email_reports.ex | 10 +-- lib/plausible/site/schema.ex | 2 +- .../{email_settings.ex => weekly_report.ex} | 4 +- .../controllers/site_controller.ex | 12 +-- .../20190911102027_add_monthly_reports.exs | 79 +++++++++++++++++++ 5 files changed, 93 insertions(+), 14 deletions(-) rename lib/plausible/site/{email_settings.ex => weekly_report.ex} (85%) create mode 100644 priv/repo/migrations/20190911102027_add_monthly_reports.exs diff --git a/lib/mix/tasks/send_email_reports.ex b/lib/mix/tasks/send_email_reports.ex index b541d1e5f..76f60d136 100644 --- a/lib/mix/tasks/send_email_reports.ex +++ b/lib/mix/tasks/send_email_reports.ex @@ -15,17 +15,17 @@ defmodule Mix.Tasks.SendEmailReports do def execute(args \\ []) do sites = Repo.all( from s in Plausible.Site, - join: es in Plausible.Site.EmailSettings, on: es.site_id == s.id, - left_join: se in "sent_email_reports", on: se.site_id == s.id and se.year == fragment("EXTRACT(year from (now() at time zone ?))", s.timezone) and se.week == fragment("EXTRACT(week from (now() at time zone ?))", s.timezone), + 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(year from (now() at time zone ?))", s.timezone) and se.week == fragment("EXTRACT(week from (now() at time zone ?))", 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 select: s, - preload: [email_settings: es] + preload: [weekly_report: wr] ) for site <- sites do - email = site.email_settings.email + email = site.weekly_report.email IO.puts("Sending email report for #{site.domain} to #{email}") send_report(email, site) end @@ -58,7 +58,7 @@ defmodule Mix.Tasks.SendEmailReports do defp email_report_sent(site) do {year, week} = Timex.now(site.timezone) |> DateTime.to_date |> Timex.iso_week - Repo.insert_all("sent_email_reports", [%{ + Repo.insert_all("sent_weekly_reports", [%{ site_id: site.id, year: year, week: week, diff --git a/lib/plausible/site/schema.ex b/lib/plausible/site/schema.ex index 7680cb21a..0cc8eecfc 100644 --- a/lib/plausible/site/schema.ex +++ b/lib/plausible/site/schema.ex @@ -11,7 +11,7 @@ defmodule Plausible.Site do many_to_many :members, User, join_through: Plausible.Site.Membership has_one :google_auth, GoogleAuth - has_one :email_settings, Plausible.Site.EmailSettings + has_one :weekly_report, Plausible.Site.WeeklyReport timestamps() end diff --git a/lib/plausible/site/email_settings.ex b/lib/plausible/site/weekly_report.ex similarity index 85% rename from lib/plausible/site/email_settings.ex rename to lib/plausible/site/weekly_report.ex index aff1b928f..941a51875 100644 --- a/lib/plausible/site/email_settings.ex +++ b/lib/plausible/site/weekly_report.ex @@ -1,9 +1,9 @@ -defmodule Plausible.Site.EmailSettings do +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 "email_settings" do + schema "weekly_reports" do field :email, :string belongs_to :site, Plausible.Site diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 30b8b84cc..1636525b8 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -42,8 +42,8 @@ defmodule PlausibleWeb.SiteController do !google_site["error"] end - report = Repo.get_by(Plausible.Site.EmailSettings, site_id: site.id) - report_changeset = report && Plausible.Site.EmailSettings.changeset(report, %{}) + report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + report_changeset = report && Plausible.Site.WeeklyReport.changeset(report, %{}) changeset = Plausible.Site.changeset(site, %{}) @@ -58,10 +58,10 @@ defmodule PlausibleWeb.SiteController do end - def update_email_settings(conn, %{"website" => website, "email_settings" => email_settings}) do + def update_email_settings(conn, %{"website" => website, "weekly_report" => weekly_report}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) - Repo.get_by(Plausible.Site.EmailSettings, site_id: site.id) - |> Plausible.Site.EmailSettings.changeset(email_settings) + Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) + |> Plausible.Site.WeeklyReport.changeset(weekly_report) |> Repo.update! conn @@ -125,7 +125,7 @@ defmodule PlausibleWeb.SiteController do def enable_email_report(conn, %{"website" => website}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) - Plausible.Site.EmailSettings.changeset(%Plausible.Site.EmailSettings{}, %{ + Plausible.Site.WeeklyReport.changeset(%Plausible.Site.WeeklyReport{}, %{ site_id: site.id, email: conn.assigns[:current_user].email }) diff --git a/priv/repo/migrations/20190911102027_add_monthly_reports.exs b/priv/repo/migrations/20190911102027_add_monthly_reports.exs new file mode 100644 index 000000000..122b95e4f --- /dev/null +++ b/priv/repo/migrations/20190911102027_add_monthly_reports.exs @@ -0,0 +1,79 @@ +defmodule Plausible.Repo.Migrations.AddMonthlyReports do + use Ecto.Migration + use Plausible.Repo + + def up do + drop constraint(:email_settings, "email_settings_site_id_fkey") + drop constraint(:email_settings, "email_settings_pkey") + execute "DROP INDEX email_settings_site_id_index" + + rename table(:email_settings), to: table(:weekly_reports) + + alter table(:weekly_reports) do + modify :id, :bigint, primary_key: true + modify :site_id, references(:sites, on_delete: :delete_all), null: false + end + + execute "ALTER SEQUENCE email_settings_id_seq RENAME TO weekly_reports_id_seq;" + create unique_index(:weekly_reports, :site_id) + + drop constraint(:sent_email_reports, "sent_email_reports_site_id_fkey") + drop constraint(:sent_email_reports, "sent_email_reports_pkey") + + rename table(:sent_email_reports), to: table(:sent_weekly_reports) + + alter table(:sent_weekly_reports) do + modify :id, :bigint, primary_key: true + modify :site_id, references(:sites, on_delete: :delete_all), null: false + end + + execute "ALTER SEQUENCE sent_email_reports_id_seq RENAME TO sent_weekly_reports_id_seq;" + + create table(:monthly_reports) do + add :site_id, references(:sites, on_delete: :delete_all), null: false + add :email, :citext, null: false + + timestamps() + end + + create table(:sent_monthly_reports) do + add :site_id, references(:sites, on_delete: :delete_all), null: false + add :year, :integer, null: false + add :month, :integer, null: false + + add :timestamp, :naive_datetime + end + end + + def down do + drop constraint(:weekly_reports, "weekly_reports_site_id_fkey") + drop constraint(:weekly_reports, "weekly_reports_pkey") + execute "DROP INDEX weekly_reports_site_id_index" + + rename table(:weekly_reports), to: table(:email_settings) + + alter table(:email_settings) do + modify :id, :bigint, primary_key: true + modify :site_id, references(:sites, on_delete: :delete_all), null: false + end + + execute "ALTER SEQUENCE weekly_reports_id_seq RENAME TO email_settings_id_seq;" + create unique_index(:email_settings, :site_id) + + + drop constraint(:sent_weekly_reports, "sent_weekly_reports_site_id_fkey") + drop constraint(:sent_weekly_reports, "sent_weekly_reports_pkey") + + rename table(:sent_weekly_reports), to: table(:sent_email_reports) + + alter table(:sent_email_reports) do + modify :id, :bigint, primary_key: true + modify :site_id, references(:sites, on_delete: :delete_all), null: false + end + + execute "ALTER SEQUENCE sent_weekly_reports_id_seq RENAME TO sent_email_reports_id_seq;" + + drop table(:monthly_reports) + drop table(:sent_monthly_reports) + end +end From ac267c5f698ab28474680772b4c8a7fb5e480101 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Mon, 9 Sep 2019 12:37:57 +0100 Subject: [PATCH 2/3] Manage monthly reports --- lib/plausible/site/monthly_report.ex | 20 +++++ .../controllers/site_controller.ex | 76 ++++++++++++++----- lib/plausible_web/router.ex | 9 ++- .../templates/site/settings.html.eex | 46 +++++++---- 4 files changed, 114 insertions(+), 37 deletions(-) create mode 100644 lib/plausible/site/monthly_report.ex diff --git a/lib/plausible/site/monthly_report.ex b/lib/plausible/site/monthly_report.ex new file mode 100644 index 000000000..e98fd91be --- /dev/null +++ b/lib/plausible/site/monthly_report.ex @@ -0,0 +1,20 @@ +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 + belongs_to :site, Plausible.Site + + timestamps() + end + + def changeset(settings, attrs \\ %{}) do + settings + |> cast(attrs, [:site_id, :email]) + |> validate_required([:site_id, :email]) + |> validate_format(:email, @mail_regex) + |> unique_constraint(:site) + end +end diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 1636525b8..04eb6feae 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -42,33 +42,22 @@ defmodule PlausibleWeb.SiteController do !google_site["error"] end - report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - report_changeset = report && Plausible.Site.WeeklyReport.changeset(report, %{}) - - changeset = Plausible.Site.changeset(site, %{}) + 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, %{}) conn |> assign(:skip_plausible_tracking, true) |> render("settings.html", site: site, - report_changeset: report_changeset, + weekly_report_changeset: weekly_report_changeset, + monthly_report_changeset: monthly_report_changeset, google_search_console_verified: google_search_console_verified, - changeset: changeset + changeset: Plausible.Site.changeset(site, %{}) ) end - - def update_email_settings(conn, %{"website" => website, "weekly_report" => weekly_report}) 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) - |> Repo.update! - - conn - |> put_flash(:success, "Email address saved succesfully") - |> redirect(to: "/#{site.domain}/settings") - end - def update_settings(conn, %{"website" => website, "site" => site_params}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) changeset = site |> Plausible.Site.changeset(site_params) @@ -122,7 +111,7 @@ defmodule PlausibleWeb.SiteController do |> redirect(to: "/" <> site.domain <> "/settings") end - def enable_email_report(conn, %{"website" => website}) do + def enable_weekly_report(conn, %{"website" => website}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) Plausible.Site.WeeklyReport.changeset(%Plausible.Site.WeeklyReport{}, %{ @@ -136,15 +125,60 @@ defmodule PlausibleWeb.SiteController do |> redirect(to: "/" <> site.domain <> "/settings") end - def disable_email_report(conn, %{"website" => website}) do + def disable_weekly_report(conn, %{"website" => website}) do site = Sites.get_for_user!(conn.assigns[:current_user].id, website) - Repo.delete_all(from es in Plausible.Site.EmailSettings, where: es.site_id == ^site.id) + Repo.delete_all(from wr in Plausible.Site.WeeklyReport, where: wr.site_id == ^site.id) conn |> put_flash(:success, "Success! You will not receive weekly email reports going forward") |> redirect(to: "/" <> site.domain <> "/settings") end + def update_weekly_settings(conn, %{"website" => website, "weekly_report" => weekly_report}) 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) + |> Repo.update! + + conn + |> put_flash(:success, "Email address saved succesfully") + |> redirect(to: "/#{site.domain}/settings") + end + + def enable_monthly_report(conn, %{"website" => website}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + + Plausible.Site.MonthlyReport.changeset(%Plausible.Site.MonthlyReport{}, %{ + site_id: site.id, + email: 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") + end + + def disable_monthly_report(conn, %{"website" => website}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + Repo.delete_all(from mr in Plausible.Site.MonthlyReport, where: mr.site_id == ^site.id) + + conn + |> put_flash(:success, "Success! You will not receive monthly email reports going forward") + |> redirect(to: "/" <> site.domain <> "/settings") + end + + def update_monthly_settings(conn, %{"website" => website, "monthly_report" => monthly_report}) 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) + |> Repo.update! + + conn + |> put_flash(:success, "Email address saved succesfully") + |> redirect(to: "/#{site.domain}/settings") + end + defp insert_site(user_id, params) do site_changeset = Plausible.Site.changeset(%Plausible.Site{}, params) diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index bf03f0cca..9195f029a 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -85,9 +85,12 @@ defmodule PlausibleWeb.Router do post "/sites", SiteController, :create_site post "/sites/:website/make-public", SiteController, :make_public post "/sites/:website/make-private", SiteController, :make_private - post "/sites/:website/email-report/enable", SiteController, :enable_email_report - post "/sites/:website/email-report/disable", SiteController, :disable_email_report - put "/sites/:website/email-report", SiteController, :update_email_settings + 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/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 get "/:website/snippet", SiteController, :add_snippet get "/:website/settings", SiteController, :settings put "/:website/settings", SiteController, :update_settings diff --git a/lib/plausible_web/templates/site/settings.html.eex b/lib/plausible_web/templates/site/settings.html.eex index 40ea88adf..43a1108ac 100644 --- a/lib/plausible_web/templates/site/settings.html.eex +++ b/lib/plausible_web/templates/site/settings.html.eex @@ -102,28 +102,48 @@
- <%= if @report_changeset do %> - <%= button(to: "/sites/#{@site.domain}/email-report/disable", method: :post, class: "border rounded-full border-grey flex items-center cursor-pointer w-8 bg-green justify-end") do %> + <%= if @weekly_report_changeset 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 %> - Receive a weekly email report every Monday <% else %> - <%= button(to: "/sites/#{@site.domain}/email-report/enable", method: :post, class: "border rounded-full border-grey flex items-center cursor-pointer w-8 justify-start") do %> + <%= button(to: "/sites/#{@site.domain}/weekly-report/enable", method: :post, class: "border rounded-full border-grey flex items-center cursor-pointer w-8 justify-start") do %> <% end %> - Receive a weekly email report every Monday <% end %> + Receive a weekly email report every Monday
- <%= if @report_changeset do %> -
+ <%= if @weekly_report_changeset do %>
- <%= form_for @report_changeset, "/sites/#{@site.domain}/email-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" %> - <%= error_tag f, :email %> + <%= 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" %> +
+ <% end %> +
+ <% end %> +
+
+ <%= if @monthly_report_changeset 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 %> + <% else %> + <%= button(to: "/sites/#{@site.domain}/monthly-report/enable", method: :post, class: "border rounded-full border-grey flex items-center cursor-pointer w-8 justify-start") do %> + + <% end %> + <% end %> + Receive a monthly email report on 1st of the month +
+ <%= if @monthly_report_changeset do %> +
+ <%= form_for @monthly_report_changeset, "/sites/#{@site.domain}/monthly-report", [class: "max-w-xs"], fn f -> %> +
+ <%= 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" %>
- <%= submit "Set email address", class: "button mt-2" %> <% end %>
<% end %> From 2d2cca2b06a439ce1febd6d53188205400eb323a Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Wed, 11 Sep 2019 16:35:30 +0100 Subject: [PATCH 3/3] Send monthly reports --- lib/mix/tasks/send_email_reports.ex | 55 ++++++++++++++++++++++++----- lib/plausible/site/schema.ex | 1 + 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/lib/mix/tasks/send_email_reports.ex b/lib/mix/tasks/send_email_reports.ex index 76f60d136..ecaac6ed8 100644 --- a/lib/mix/tasks/send_email_reports.ex +++ b/lib/mix/tasks/send_email_reports.ex @@ -13,6 +13,11 @@ defmodule Mix.Tasks.SendEmailReports do 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() + end + + defp send_weekly_emails() do sites = Repo.all( from s in Plausible.Site, join: wr in Plausible.Site.WeeklyReport, on: wr.site_id == s.id, @@ -20,19 +25,44 @@ defmodule Mix.Tasks.SendEmailReports do 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 - select: s, preload: [weekly_report: wr] ) for site <- sites do email = site.weekly_report.email - IO.puts("Sending email report for #{site.domain} to #{email}") - send_report(email, site) + query = Plausible.Stats.Query.from(site.timezone, %{"period" => "7d"}) + + IO.puts("Sending weekly report for #{site.domain} to #{email}") + + send_report(email, site, query) + weekly_report_sent(site) end end - defp send_report(email, site) do - query = Plausible.Stats.Query.from(site.timezone, %{"period" => "7d"}) + defp send_monthly_emails() 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), + 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 + 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}) + + IO.puts("Sending monthly report for #{site.domain} to #{email}") + + send_report(email, site, query) + monthly_report_sent(site) + 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) @@ -51,11 +81,9 @@ defmodule Mix.Tasks.SendEmailReports do pages: pages, query: query ) |> Plausible.Mailer.deliver_now() - - email_report_sent(site) end - defp email_report_sent(site) do + defp weekly_report_sent(site) do {year, week} = Timex.now(site.timezone) |> DateTime.to_date |> Timex.iso_week Repo.insert_all("sent_weekly_reports", [%{ @@ -65,4 +93,15 @@ defmodule Mix.Tasks.SendEmailReports do timestamp: Timex.now() }]) end + + defp monthly_report_sent(site) do + date = Timex.now(site.timezone) |> DateTime.to_date + + Repo.insert_all("sent_monthly_reports", [%{ + site_id: site.id, + year: date.year, + month: date.month, + timestamp: Timex.now() + }]) + end end diff --git a/lib/plausible/site/schema.ex b/lib/plausible/site/schema.ex index 0cc8eecfc..8f7f9082f 100644 --- a/lib/plausible/site/schema.ex +++ b/lib/plausible/site/schema.ex @@ -12,6 +12,7 @@ defmodule Plausible.Site do many_to_many :members, User, join_through: Plausible.Site.Membership has_one :google_auth, GoogleAuth has_one :weekly_report, Plausible.Site.WeeklyReport + has_one :monthly_report, Plausible.Site.MonthlyReport timestamps() end