From 79ef60bbd9ac5c3f7792cb1a482bbc9e04491cee Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Tue, 14 Jul 2026 17:11:45 -0400 Subject: [PATCH 1/3] refactor & test homepage alerts --- lib/dotcom/alerts.ex | 80 +++++++++++++++++++++++++++++++ lib/dotcom_web/views/page_view.ex | 72 ++++------------------------ test/dotcom/alerts_test.exs | 57 ++++++++++++++++++++++ 3 files changed, 146 insertions(+), 63 deletions(-) diff --git a/lib/dotcom/alerts.ex b/lib/dotcom/alerts.ex index f27df0b92c..2011d13197 100644 --- a/lib/dotcom/alerts.ex +++ b/lib/dotcom/alerts.ex @@ -262,4 +262,84 @@ defmodule Dotcom.Alerts do %{} -> false end) end + + def routes_with_high_priority_alerts_by_mode(alerts) do + modes = [:subway, :bus, :commuter_rail, :ferry] + empty_by_mode = Map.new(modes, fn mode -> {mode, MapSet.new()} end) + + route_ids_by_mode = + alerts + |> Enum.filter(&(Alerts.Priority.priority(&1) == :high)) + |> Enum.reduce(empty_by_mode, fn alert, acc -> + route_ids = Alert.get_entity(alert, :route) |> MapSet.delete(nil) + + alert + |> alert_route_type() + |> Enum.map(&Route.type_atom/1) + |> Enum.reduce(acc, fn mode, acc2 -> + Map.update!(acc2, mode, &MapSet.union(&1, route_ids)) + end) + end) + + Enum.map(modes, fn mode_key -> + route_ids = + route_ids_by_mode + |> Map.fetch!(mode_key) + |> MapSet.to_list() + + {mode_key, + get_many(route_ids, &@routes_repo_module.get/1) + |> Stream.filter(&match?({:ok, %Route{}}, &1)) + |> Stream.map(fn {:ok, route} -> route end) + |> Enum.sort_by(& &1.sort_order)} + end) + end + + defp get_many([], _), do: [] + + defp get_many(ids, func) do + Task.async_stream(ids, func, max_concurrency: 8, on_timeout: :kill_task, ordered: false) + end + + def stops_with_access_alerts_by_effect(alerts) do + access_effects = Alerts.Accessibility.effect_types() + empty_by_effect = Map.new(access_effects, &{&1, MapSet.new()}) + + stop_ids_by_effect = + alerts + |> Enum.reduce(empty_by_effect, fn alert, acc -> + if Map.has_key?(acc, alert.effect) do + stop_id = alert_stop_ids(alert) |> List.last() + Map.update!(acc, alert.effect, &MapSet.put(&1, stop_id)) + else + acc + end + end) + + Enum.map(access_effects, fn effect -> + stops = + stop_ids_by_effect + |> Map.fetch!(effect) + |> get_many(&@stops_repo_module.get_parent/1) + |> Stream.filter(&match?({:ok, %Stop{}}, &1)) + |> Stream.map(fn {:ok, stop} -> stop end) + |> Enum.sort_by(& &1.name) + + {effect, stops} + end) + end + + def alert_route_type(alert) do + alert + |> Alert.get_entity(:route_type) + |> MapSet.delete(nil) + |> MapSet.to_list() + end + + def alert_stop_ids(alert) do + alert + |> Alert.get_entity(:stop) + |> MapSet.delete(nil) + |> MapSet.to_list() + end end diff --git a/lib/dotcom_web/views/page_view.ex b/lib/dotcom_web/views/page_view.ex index a04181c885..60d3973325 100644 --- a/lib/dotcom_web/views/page_view.ex +++ b/lib/dotcom_web/views/page_view.ex @@ -11,73 +11,19 @@ defmodule DotcomWeb.PageView do alias CMS.Partial.Banner alias DotcomWeb.PartialView - @stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops] - @spec get_route(Routes.Route.id_t()) :: Routes.Route.t() | nil - def get_route(id) do - case DotcomWeb.ScheduleController.Line.Helpers.get_route(id) do - {:ok, route} -> route - _ -> nil - end - end - - @spec sort_routes({Routes.Route.gtfs_route_type(), [Routes.Route.t()]}) :: - {Routes.Route.gtfs_route_type(), [Routes.Route.t()]} - defp sort_routes({mode, routes}) do - {mode, Enum.sort_by(routes, & &1.sort_order)} - end - - @spec get_mode_order({Routes.Route.gtfs_route_type(), [Routes.Route.t()]}) :: integer() - defp get_mode_order({:subway, _}), do: 0 - defp get_mode_order({:bus, _}), do: 1 - defp get_mode_order({:commuter_rail, _}), do: 2 - defp get_mode_order({:ferry, _}), do: 3 - - @spec get_access_issue_order({Alerts.Accessibility.effect_type(), [Stops.Stop.t()]}) :: - integer() - defp get_access_issue_order({:elevator_closure, _}), do: 0 - defp get_access_issue_order({:escalator_closure, _}), do: 1 - defp get_access_issue_order({:access_issue, _}), do: 2 - @spec alerts([Alerts.Alert.t()]) :: Phoenix.HTML.Safe.t() def alerts(alerts) do - routes_with_high_priority_alerts_by_mode = - alerts - |> Enum.filter(&(Alerts.Priority.priority(&1) == :high)) - |> Enum.reduce(MapSet.new(), fn alert, routes -> - MapSet.union(routes, Alerts.Alert.get_entity(alert, :route)) - end) - |> Enum.filter(& &1) - |> Enum.map(&get_route/1) - |> Enum.filter(& &1) - |> Enum.group_by(&Routes.Route.type_atom(&1.type)) - |> (&Map.merge(%{bus: [], subway: [], ferry: [], commuter_rail: []}, &1)).() - |> Enum.map(&sort_routes/1) - |> Enum.sort_by(&get_mode_order/1) - - stops_with_accessibility_alerts_by_issue = - alerts - |> Enum.filter(&Alerts.Accessibility.accessibility?/1) - |> Enum.reduce( - Map.new(Alerts.Accessibility.effect_types(), fn t -> {t, MapSet.new()} end), - fn alert, types -> - stops = Alerts.Alert.get_entity(alert, :stop) - type = alert.effect - - Map.put(types, type, MapSet.union(Map.get(types, type), stops)) - end - ) - |> Enum.map(fn {type, stops} -> - {type, - Enum.map(stops, &@stops_repo.get_parent/1) - |> Enum.filter(& &1) - |> Enum.uniq_by(& &1.id) - |> Enum.sort_by(& &1.name)} - end) - |> Enum.sort_by(&get_access_issue_order/1) + [routes, stops] = + [ + &Dotcom.Alerts.routes_with_high_priority_alerts_by_mode/1, + &Dotcom.Alerts.stops_with_access_alerts_by_effect/1 + ] + |> Task.async_stream(& &1.(alerts), timeout: 10_000) + |> Enum.map(fn {:ok, result} -> result end) render("_alerts.html", - routes_with_high_priority_alerts_by_mode: routes_with_high_priority_alerts_by_mode, - stops_with_accessibility_alerts_by_issue: stops_with_accessibility_alerts_by_issue + routes_with_high_priority_alerts_by_mode: routes, + stops_with_accessibility_alerts_by_issue: stops ) end diff --git a/test/dotcom/alerts_test.exs b/test/dotcom/alerts_test.exs index 554ae643c5..0b32e07883 100644 --- a/test/dotcom/alerts_test.exs +++ b/test/dotcom/alerts_test.exs @@ -491,4 +491,61 @@ defmodule Dotcom.AlertsTest do assert systemwide_mode_alert?(alert_without_route, mode) end + + describe "routes_with_high_priority_alerts_by_mode/1" do + setup do + {:ok, %{alerts: Factories.Alerts.Alert.build_list(50, :alert)}} + end + + test "builds list of routes by mode", %{alerts: alerts} do + stub(Routes.Repo.Mock, :get, fn _ -> Factories.Routes.Route.build(:route) end) + + for {mode, routes} <- routes_with_high_priority_alerts_by_mode(alerts) do + # valid mode + assert Routes.Route.types_for_mode(mode) + + if routes != [] do + assert [%Routes.Route{} | _] = routes + end + end + end + + test "doesn't error if nil routes", %{alerts: alerts} do + stub(Routes.Repo.Mock, :get, fn _ -> nil end) + + assert routes_with_high_priority_alerts_by_mode(alerts) == [ + subway: [], + bus: [], + commuter_rail: [], + ferry: [] + ] + end + end + + describe "stops_with_access_alerts_by_effect/1" do + setup do + {:ok, %{alerts: Factories.Alerts.Alert.build_list(50, :alert)}} + end + + test "builds list of stops by accessibility effect", %{alerts: alerts} do + stub(Stops.Repo.Mock, :get_parent, fn _ -> Factories.Stops.Stop.build(:stop) end) + + stops_by_effect = stops_with_access_alerts_by_effect(alerts) + + for {effect, stops} <- stops_by_effect do + assert effect in Alerts.Accessibility.effect_types() + + if stops != [] do + assert [%Stops.Stop{} | _] = stops + end + end + end + + test "doesn't error if nil stops", %{alerts: alerts} do + stub(Stops.Repo.Mock, :get_parent, fn _ -> nil end) + + stops_by_effect = stops_with_access_alerts_by_effect(alerts) + assert stops_by_effect == [elevator_closure: [], escalator_closure: [], access_issue: []] + end + end end From c2c5a8c6b1cd6c7dd553bdb28ef1e5883272363d Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Tue, 14 Jul 2026 17:11:56 -0400 Subject: [PATCH 2/3] feat: add way to get all alerts without sorting --- lib/alerts/cache/store.ex | 6 ++++++ lib/dotcom_web/controllers/page_controller.ex | 5 +---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/alerts/cache/store.ex b/lib/alerts/cache/store.ex index c5d088713b..c39c71093f 100644 --- a/lib/alerts/cache/store.ex +++ b/lib/alerts/cache/store.ex @@ -103,6 +103,12 @@ defmodule Alerts.Cache.Store do |> List.first() end + def current_alerts_unsorted do + :alert_id_to_alert + |> :ets.select([{{:_, :"$1"}, [], [:"$1"]}]) + |> Enum.filter(&Dotcom.Alerts.in_effect_now?/1) + end + @doc """ Retrieves the full set of current alerts in priority sorted order. """ diff --git a/lib/dotcom_web/controllers/page_controller.ex b/lib/dotcom_web/controllers/page_controller.ex index 11e21f1978..0e66b410d0 100644 --- a/lib/dotcom_web/controllers/page_controller.ex +++ b/lib/dotcom_web/controllers/page_controller.ex @@ -27,7 +27,6 @@ defmodule DotcomWeb.PageController do {promoted, remainder} = whats_happening_items() banner = banner() date = conn.assigns.date - date_time = conn.assigns.date_time conn |> assign( @@ -42,9 +41,7 @@ defmodule DotcomWeb.PageController do |> async_assign_default( :alerts, fn -> - date_time - |> Alerts.Repo.all() - |> Enum.filter(&Alerts.Match.any_time_match?(&1, date_time)) + Alerts.Cache.Store.current_alerts_unsorted() end, [] ) From c03b1c561af4443394f62c19ae8b0bf8186e80c0 Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Wed, 15 Jul 2026 17:07:50 -0400 Subject: [PATCH 3/3] Revert "feat: add way to get all alerts without sorting" This reverts commit c2c5a8c6b1cd6c7dd553bdb28ef1e5883272363d. --- lib/alerts/cache/store.ex | 6 ------ lib/dotcom_web/controllers/page_controller.ex | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/alerts/cache/store.ex b/lib/alerts/cache/store.ex index c39c71093f..c5d088713b 100644 --- a/lib/alerts/cache/store.ex +++ b/lib/alerts/cache/store.ex @@ -103,12 +103,6 @@ defmodule Alerts.Cache.Store do |> List.first() end - def current_alerts_unsorted do - :alert_id_to_alert - |> :ets.select([{{:_, :"$1"}, [], [:"$1"]}]) - |> Enum.filter(&Dotcom.Alerts.in_effect_now?/1) - end - @doc """ Retrieves the full set of current alerts in priority sorted order. """ diff --git a/lib/dotcom_web/controllers/page_controller.ex b/lib/dotcom_web/controllers/page_controller.ex index 0e66b410d0..11e21f1978 100644 --- a/lib/dotcom_web/controllers/page_controller.ex +++ b/lib/dotcom_web/controllers/page_controller.ex @@ -27,6 +27,7 @@ defmodule DotcomWeb.PageController do {promoted, remainder} = whats_happening_items() banner = banner() date = conn.assigns.date + date_time = conn.assigns.date_time conn |> assign( @@ -41,7 +42,9 @@ defmodule DotcomWeb.PageController do |> async_assign_default( :alerts, fn -> - Alerts.Cache.Store.current_alerts_unsorted() + date_time + |> Alerts.Repo.all() + |> Enum.filter(&Alerts.Match.any_time_match?(&1, date_time)) end, [] )