From f15b57781a3b859e2fa7d9a5f6925729109bbc9a Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Thu, 9 Jan 2025 11:47:55 -0500 Subject: [PATCH] feat: Add module that retrieves system-status relevant alerts --- lib/dotcom/system_status.ex | 23 +++ lib/dotcom/system_status/alerts.ex | 29 ++++ lib/dotcom_web/live/admin.ex | 5 + lib/dotcom_web/live/system_status.ex | 35 +++++ lib/dotcom_web/router.ex | 9 ++ test/dotcom/system_status/alerts_test.exs | 177 ++++++++++++++++++++++ 6 files changed, 278 insertions(+) create mode 100644 lib/dotcom/system_status.ex create mode 100644 lib/dotcom/system_status/alerts.ex create mode 100644 lib/dotcom_web/live/system_status.ex create mode 100644 test/dotcom/system_status/alerts_test.exs diff --git a/lib/dotcom/system_status.ex b/lib/dotcom/system_status.ex new file mode 100644 index 0000000000..d1e5122655 --- /dev/null +++ b/lib/dotcom/system_status.ex @@ -0,0 +1,23 @@ +defmodule Dotcom.SystemStatus do + alias Dotcom.SystemStatus + + def subway_alerts_for_today() do + subway_alerts_for_today(Timex.now()) + end + + def subway_alerts_for_today(now) do + [ + "Red", + "Orange", + "Blue", + "Green-B", + "Green-C", + "Green-D", + "Green-E", + "Mattapan" + ] + |> Alerts.Repo.by_route_ids(now) + |> SystemStatus.Alerts.for_today(now) + |> SystemStatus.Alerts.filter_relevant() + end +end diff --git a/lib/dotcom/system_status/alerts.ex b/lib/dotcom/system_status/alerts.ex new file mode 100644 index 0000000000..394632f2af --- /dev/null +++ b/lib/dotcom/system_status/alerts.ex @@ -0,0 +1,29 @@ +defmodule Dotcom.SystemStatus.Alerts do + @relevant_effects [:delay, :shuttle, :suspension, :station_closure] + + defp has_started?(active_period_start, now) do + now |> Timex.end_of_day() |> Timex.after?(active_period_start) + end + + defp has_not_ended?(nil, _now) do + true + end + + defp has_not_ended?(active_period_end, now) do + now |> Timex.before?(active_period_end) + end + + def active_today?(alert, now) do + Enum.any?(alert.active_period, fn {active_period_start, active_period_end} -> + has_started?(active_period_start, now) && has_not_ended?(active_period_end, now) + end) + end + + def for_today(alerts, now) do + Enum.filter(alerts, &active_today?(&1, now)) + end + + def filter_relevant(alerts) do + alerts |> Enum.filter(fn %{effect: effect} -> effect in @relevant_effects end) + end +end diff --git a/lib/dotcom_web/live/admin.ex b/lib/dotcom_web/live/admin.ex index 572b4a0c8a..b14b290b56 100644 --- a/lib/dotcom_web/live/admin.ex +++ b/lib/dotcom_web/live/admin.ex @@ -16,6 +16,11 @@ defmodule DotcomWeb.Live.Admin do url: Helpers.live_path(socket, DotcomWeb.Live.TripPlanner), title: "Trip Planner Preview", description: "WIP on the trip planner rewrite." + }, + %{ + url: Helpers.live_path(socket, DotcomWeb.Live.SystemStatus), + title: "System Status Widget Preview", + description: "WIP on the system status widget." } ] )} diff --git a/lib/dotcom_web/live/system_status.ex b/lib/dotcom_web/live/system_status.ex new file mode 100644 index 0000000000..8e7457ca9e --- /dev/null +++ b/lib/dotcom_web/live/system_status.ex @@ -0,0 +1,35 @@ +defmodule DotcomWeb.Live.SystemStatus do + @moduledoc """ + A temporary LiveView for showing off the system status widget until we + put it into the homepage (and elsewhere). + """ + + alias Dotcom.SystemStatus + use DotcomWeb, :live_view + + def render(assigns) do + assigns = + assigns + |> assign(:alerts, SystemStatus.subway_alerts_for_today()) + + ~H""" +
+ <.alert :for={alert <- @alerts} alert={alert} /> +
+ """ + end + + defp alert(assigns) do + ~H""" +
+ + {@alert.severity} {@alert.effect}: {@alert.header} + +
+ Raw alert +
{inspect(@alert, pretty: true)}
+
+
+ """ + end +end diff --git a/lib/dotcom_web/router.ex b/lib/dotcom_web/router.ex index f693dccf6e..1fadd9a87b 100644 --- a/lib/dotcom_web/router.ex +++ b/lib/dotcom_web/router.ex @@ -274,6 +274,15 @@ defmodule DotcomWeb.Router do end end + scope "/preview", DotcomWeb do + import Phoenix.LiveView.Router + pipe_through([:browser, :browser_live, :basic_auth_readonly]) + + live_session :system_status, layout: {DotcomWeb.LayoutView, :preview} do + live "/system-status", Live.SystemStatus + end + end + scope "/api", DotcomWeb do pipe_through([:secure, :browser]) diff --git a/test/dotcom/system_status/alerts_test.exs b/test/dotcom/system_status/alerts_test.exs new file mode 100644 index 0000000000..11ac92090d --- /dev/null +++ b/test/dotcom/system_status/alerts_test.exs @@ -0,0 +1,177 @@ +defmodule Dotcom.SystemStatus.AlertsTest do + use ExUnit.Case, async: true + + import Test.Support.Factories.Alerts.Alert + + alias Dotcom.SystemStatus.Alerts + + defp local_datetime(naive) do + DateTime.from_naive!(naive, "America/New_York") + end + + describe "active_today?/2" do + test "returns true if the alert is currently active" do + assert Alerts.active_today?( + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-09 12:00:00]), + local_datetime(~N[2025-01-09 20:00:00]) + } + ] + ), + local_datetime(~N[2025-01-09 13:00:00]) + ) + end + + test "returns false if the alert starts after end-of-service" do + refute Alerts.active_today?( + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-10 12:00:00]), + local_datetime(~N[2025-01-10 20:00:00]) + } + ] + ), + local_datetime(~N[2025-01-09 13:00:00]) + ) + end + + test "returns true if the alert starts later, but before end-of-service" do + assert Alerts.active_today?( + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-09 20:00:00]), + local_datetime(~N[2025-01-10 20:00:00]) + } + ] + ), + local_datetime(~N[2025-01-09 13:00:00]) + ) + end + + test "returns false if the alert has already ended" do + refute Alerts.active_today?( + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-09 10:00:00]), + local_datetime(~N[2025-01-09 12:00:00]) + } + ] + ), + local_datetime(~N[2025-01-09 13:00:00]) + ) + end + + test "returns true if the alert has no end time" do + alert = + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-09 10:00:00]), + nil + } + ] + ) + + assert Alerts.active_today?( + alert, + local_datetime(~N[2025-01-09 13:00:00]) + ) + end + + test "returns false if the alert has no end time but hasn't started yet" do + refute Alerts.active_today?( + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-10 10:00:00]), + nil + } + ] + ), + local_datetime(~N[2025-01-09 13:00:00]) + ) + end + + test "returns true if a later part of the alert's active period is active" do + assert Alerts.active_today?( + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-08 10:00:00]), + local_datetime(~N[2025-01-08 12:00:00]) + }, + { + local_datetime(~N[2025-01-09 10:00:00]), + local_datetime(~N[2025-01-09 12:00:00]) + } + ] + ), + local_datetime(~N[2025-01-09 11:00:00]) + ) + end + end + + describe "for_today/2" do + test "includes alerts that are active today" do + alert1 = + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-09 12:00:00]), + local_datetime(~N[2025-01-09 20:00:00]) + } + ] + ) + + alert2 = + build(:alert, + active_period: [ + { + local_datetime(~N[2025-01-10 12:00:00]), + local_datetime(~N[2025-01-10 20:00:00]) + } + ] + ) + + assert Alerts.for_today( + [alert1, alert2], + local_datetime(~N[2025-01-09 13:00:00]) + ) == [alert1] + end + end + + describe "filter_relevant/1" do + test "includes an alert if its effect is :delay" do + alert = build(:alert, effect: :delay) + assert Alerts.filter_relevant([alert]) == [alert] + end + + test "includes an alert if its effect is :shuttle" do + alert = build(:alert, effect: :shuttle) + assert Alerts.filter_relevant([alert]) == [alert] + end + + test "includes an alert if its effect is :suspension" do + alert = build(:alert, effect: :suspension) + assert Alerts.filter_relevant([alert]) == [alert] + end + + test "includes an alert if its effect is :station_closure" do + alert = build(:alert, effect: :station_closure) + assert Alerts.filter_relevant([alert]) == [alert] + end + + test "does not include alerts with other effects" do + assert Alerts.filter_relevant([ + build(:alert, effect: :policy_change), + build(:alert, effect: :extra_service), + build(:alert, effect: :stop_closure) + ]) == [] + end + end +end