-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Planned disruptions logic #2373
Changes from 68 commits
946ba02
ce6f9c1
59eeb0e
8de7cfd
1fcecd4
9d0d291
5be4845
2b9de8d
19f1523
dc8ada9
5e707d7
b454b57
c17011b
b1ff2e8
c77b10e
697bd25
4029270
cf25e6b
f4235b2
7e46668
9432320
e4d0e8a
dfcd997
b1186ee
ce97242
f890a33
04b942a
f757d30
189331e
ff57b26
5d7931d
26dd3f1
1510905
dfff32f
912da3a
bf8e064
cad2bf8
f534005
d591ba0
d8fa158
31d65f1
1c66c3c
d7e62b4
5b83bb2
f8dfc84
5aa779a
da82601
4847945
c75c844
65f4d26
d2f86fe
9c42b43
24ec694
13e38c7
f8afecb
6490bac
075ad59
2a6d31b
e35c0ad
b69d409
a4196ca
837a383
0626b87
e29f2f5
342e158
946552f
84b0098
e500528
69c0a03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
defmodule Alerts.Repo.Behaviour do | ||
@moduledoc """ | ||
Behaviour for the Alerts repo. | ||
""" | ||
|
||
alias Alerts.Alert | ||
|
||
@doc """ | ||
Return all alerts for the given route ids. | ||
""" | ||
@callback by_route_ids([String.t()], DateTime.t()) :: [Alert.t()] | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
defmodule Dotcom.Alerts do | ||
@moduledoc """ | ||
A collection of functions that help to work with alerts in a unified way. | ||
""" | ||
|
||
alias Alerts.Alert | ||
|
||
@service_impacting_effects [:delay, :shuttle, :suspension, :station_closure] | ||
|
||
@doc """ | ||
Does the alert have an effect that is considered service-impacting? | ||
""" | ||
@spec service_impacting_alert?(Alert.t()) :: boolean() | ||
def service_impacting_alert?(%Alert{effect: effect}) do | ||
effect in @service_impacting_effects | ||
end | ||
|
||
@doc """ | ||
Returns a list of the alert effects that are considered service-impacting. | ||
""" | ||
@spec service_impacting_effects() :: [atom()] | ||
def service_impacting_effects(), do: @service_impacting_effects | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
defmodule Dotcom.Alerts.Disruptions.Subway do | ||
@moduledoc """ | ||
Disruptions are alerts that have `service_impacting_effects` grouped by `service_range`. | ||
""" | ||
|
||
import Dotcom.Alerts, only: [service_impacting_alert?: 1] | ||
import Dotcom.Routes, only: [subway_route_ids: 0] | ||
import Dotcom.Utils.ServiceDateTime, only: [service_range: 1] | ||
|
||
alias Alerts.Alert | ||
alias Dotcom.Utils | ||
|
||
@alerts_repo Application.compile_env!(:dotcom, :repo_modules)[:alerts] | ||
|
||
@doc """ | ||
Disruptions that occur any time after today's service range. | ||
""" | ||
@spec future_disruptions() :: %{Utils.ServiceDateTime.named_service_range() => [Alert.t()]} | ||
def future_disruptions() do | ||
disruption_groups() |> Map.take([:later_this_week, :next_week, :after_next_week]) | ||
end | ||
|
||
@doc """ | ||
Disruptions that occur during today's service range. | ||
""" | ||
@spec todays_disruptions() :: %{today: [Alert.t()]} | ||
def todays_disruptions() do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I may be reading this wrong, but it looks like this will include alerts from earlier today, even if their effect periods have ended. If so, we should double-check the requirements, because I had thought that we only want to include alerts if they're either currently active or will be active later in the day. Either way, I think it would be good to have a test explicitly highlighting the "earlier-today" alerts behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is grabbing today's disruptions. If the view doesn't want ones that are earlier than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And, it will only get those alerts if the effect period ended after the start of the service day. So, the disruption effected 'today'. |
||
disruption_groups() |> Map.take([:today]) | ||
end | ||
|
||
# Groups all disruption alerts by service range. | ||
# | ||
# 1. Gets all alerts for subway routes. | ||
# 2. Filters out non-service-impacting alerts | ||
# 3. Groups them according to service range. | ||
defp disruption_groups() do | ||
subway_route_ids() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this will work correctly (unless I missed a change in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was pulling Green Line alerts with this code. I can check to see if we want Mattapan alerts to show up in this component. Right now it only shows the main lines. Easy to add if we need/want them. |
||
|> @alerts_repo.by_route_ids(Utils.DateTime.now()) | ||
|> Enum.filter(&service_impacting_alert?/1) | ||
|> Enum.reduce(%{}, &group_alerts/2) | ||
end | ||
|
||
# Looks at every active period for an alert and groups that alert by service range. | ||
# Alerts can overlap service ranges, in which case we want them to appear in both. | ||
defp group_alerts(alert, groups) do | ||
alert | ||
|> Map.get(:active_period) | ||
|> Enum.map(fn {start, stop} -> [service_range(start), service_range(stop)] end) | ||
|> List.flatten() | ||
|> Enum.uniq() | ||
|> Enum.reduce(groups, fn service_range, groups -> | ||
Map.update(groups, service_range, [alert], &(&1 ++ [alert])) | ||
end) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
defmodule Dotcom.Routes do | ||
@moduledoc """ | ||
A collection of functions that help to work with routes in a unified way. | ||
""" | ||
|
||
@subway_route_ids ["Blue", "Green", "Orange", "Red"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can see a future where we work branches/lines/routes and relationships into here in some way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couple of thoughts on this line of code in particular: Idea: Would it be worth having a type for line ID that only allows these four values? Then the type of Also, the name And finally - love extracting this and putting it somewhere clean and universal! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eventually, we won't need this because we'll just be able to query routes by type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And then query alerts by whatever attributes. |
||
|
||
@doc """ | ||
Returns a list of route ids for all subway routes. | ||
""" | ||
@spec subway_route_ids() :: [String.t()] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Praise This is a better place for this than where it was - nice to see this attr gradually migrating to its optimal home 🏠 |
||
def subway_route_ids(), do: @subway_route_ids | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,13 +5,9 @@ defmodule Dotcom.SystemStatus.Alerts do | |
belong in the main `Alerts` module. | ||
""" | ||
|
||
import Dotcom.Alerts, only: [service_impacting_alert?: 1] | ||
|
||
@type service_effect_t :: :delay | :shuttle | :suspension | :station_closure | ||
@service_effects [:delay, :shuttle, :suspension, :station_closure] | ||
@doc """ | ||
Returns a list of the alert effects that are considered | ||
service-impacting. | ||
""" | ||
def service_effects(), do: @service_effects | ||
|
||
@doc """ | ||
Returns `true` if the alert is active at some point during the day | ||
|
@@ -92,7 +88,7 @@ defmodule Dotcom.SystemStatus.Alerts do | |
} | ||
""" | ||
def filter_relevant(alerts) do | ||
alerts |> Enum.filter(fn %{effect: effect} -> effect in @service_effects end) | ||
alerts |> Enum.filter(&service_impacting_alert?/1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! I could eventually see this whole module getting lumped into |
||
end | ||
|
||
# Returns true if the alert (as signified by the active_period_start provided) | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -39,6 +39,8 @@ defmodule Dotcom.Utils.DateTime do | |||||
Timex will return an error if the time occurs when we "spring-forward" in DST transitions. | ||||||
That is because one hour does not occur--02:00:00am to 03:00:00am. | ||||||
In that case, we set the time to 03:00:00am. | ||||||
|
||||||
If we are given something tha tis not a DateTime, AmbiguousDateTime, or an error tuple, we log the input and return `now`. | ||||||
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Begone! |
||||||
""" | ||||||
@impl Behaviour | ||||||
def coerce_ambiguous_date_time(%DateTime{} = date_time), do: date_time | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
# Planned Disruptions | ||
joshlarson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Section | ||
|
||
```elixir | ||
defmodule Dotcom.PlannedDisruptions.Display do | ||
@moduledoc false | ||
|
||
def buckets_to_output(buckets) do | ||
Enum.each(buckets, &bucket_to_output/1) | ||
end | ||
|
||
defp alert_to_lines(alert) do | ||
Enum.map(alert.informed_entity.entities, & &1.route) | ||
|> Enum.uniq() | ||
|> Enum.sort() | ||
|> Enum.join(" / ") | ||
end | ||
|
||
def alert_to_output(alert) do | ||
{start, stop} = time_range(alert.active_period) | ||
lines = alert_to_lines(alert) | ||
effect = alert.effect |> Atom.to_string() |> String.upcase() | ||
|
||
IO.puts("#{lines} [#{effect}] #{format_date(start)}-#{format_date(stop)}") | ||
IO.puts("---------------") | ||
IO.puts(alert.header) | ||
IO.puts("\n") | ||
end | ||
|
||
defp bucket_to_output({key, alerts}) do | ||
key |> Atom.to_string() |> Recase.to_title() |> IO.puts() | ||
|
||
IO.puts("===============") | ||
|
||
Enum.each(alerts, &alert_to_output/1) | ||
|
||
IO.puts("\n") | ||
end | ||
|
||
defp format_date(datetime) do | ||
datetime |> Util.service_date() |> Timex.format!("%a %b %d", :strftime) | ||
end | ||
|
||
defp time_range(active_periods) do | ||
periods = Enum.sort_by(active_periods, fn {start, _} -> start end) | ||
|
||
{start, _} = List.first(periods) | ||
{_, stop} = List.last(periods) | ||
|
||
{start, stop} | ||
end | ||
end | ||
``` | ||
|
||
```elixir | ||
defmodule DotcomWeb.Components.PlannedDisruptions do | ||
use Phoenix.Component | ||
|
||
import MbtaMetro.Components.Accordion, only: [accordion: 1] | ||
|
||
def disruptions(assigns) do | ||
~H""" | ||
<div :for={{service_range, alerts} <- @disruptions}> | ||
<h2><%= service_range_string(service_range) %></h2> | ||
<.disruption :for={alert <- alerts} alert={alert} /> | ||
</div> | ||
""" | ||
end | ||
|
||
defp disruption(assigns) do | ||
~H""" | ||
<.accordion > | ||
<:heading> | ||
<.heading alert={@alert} /> | ||
</:heading> | ||
<:content> | ||
<.content alert={@alert} /> | ||
</:content> | ||
</.accordion> | ||
""" | ||
end | ||
|
||
defp heading(assigns) do | ||
~H""" | ||
<% {start, stop} = alert_date_time_range(@alert) %> | ||
<%= "[#{@alert.effect}] #{format_date(start)}-#{format_date(stop)}" %> | ||
""" | ||
end | ||
|
||
defp content(assigns) do | ||
~H""" | ||
|
||
""" | ||
end | ||
|
||
defp alert_date_time_range(%Alerts.Alert{active_period: active_period}) do | ||
periods = Enum.sort_by(active_period, fn {start, _} -> start end) | ||
|
||
{start, _} = List.first(periods) | ||
{_, stop} = List.last(periods) | ||
|
||
{start, stop} | ||
end | ||
|
||
defp format_date(datetime) do | ||
datetime |> Util.service_date() |> Timex.format!("%a %b %d", :strftime) | ||
end | ||
|
||
defp service_range_string(service_range) do | ||
service_range | ||
|> Atom.to_string() | ||
|> Recase.to_title() | ||
end | ||
end | ||
|
||
alias DotcomWeb.Components.PlannedDisruptions | ||
|
||
assigns = %{ | ||
disruptions: Dotcom.Alerts.Disruptions.Subway.future_disruptions() | ||
} | ||
|
||
PlannedDisruptions.disruptions(assigns) |> KinoLiveComponent.component() | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
defmodule Dotcom.Alerts.Disruptions.SubwayTest do | ||
use ExUnit.Case | ||
|
||
import Dotcom.Alerts, only: [service_impacting_effects: 0] | ||
import Dotcom.Alerts.Disruptions.Subway | ||
|
||
import Dotcom.Utils.ServiceDateTime, | ||
only: [ | ||
service_range_day: 0, | ||
service_range_later_this_week: 0, | ||
service_range_next_week: 0, | ||
service_range_after_next_week: 0 | ||
] | ||
|
||
import Mox | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forget the |
||
|
||
alias Test.Support.Factories | ||
|
||
setup :verify_on_exit! | ||
|
||
setup _ do | ||
stub_with(Dotcom.Utils.DateTime.Mock, Dotcom.Utils.DateTime) | ||
|
||
:ok | ||
end | ||
|
||
describe "future_disruptions/0" do | ||
test "returns an empty map when there are no alerts" do | ||
expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now -> | ||
[] | ||
end) | ||
|
||
# Exercise/Verify | ||
assert %{} = future_disruptions() | ||
end | ||
|
||
test "returns alerts for later this week, next week, and after next week" do | ||
# Setup | ||
alert_today = service_range_day() |> disruption_alert() | ||
alert_later_this_week = service_range_later_this_week() |> disruption_alert() | ||
alert_next_week = service_range_next_week() |> disruption_alert() | ||
|
||
{alert_after_next_week_start, _} = service_range_after_next_week() | ||
|
||
alert_after_next_week = | ||
{alert_after_next_week_start, Timex.shift(alert_after_next_week_start, days: 1)} | ||
|> disruption_alert() | ||
|
||
expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now -> | ||
[alert_today, alert_later_this_week, alert_next_week, alert_after_next_week] | ||
end) | ||
|
||
# Exercise/Verify | ||
assert %{ | ||
later_this_week: [^alert_later_this_week], | ||
next_week: [^alert_next_week], | ||
after_next_week: [^alert_after_next_week] | ||
} = future_disruptions() | ||
end | ||
end | ||
|
||
describe "todays_disruptions/0" do | ||
test "returns an empty map when there are no alerts" do | ||
expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now -> | ||
[] | ||
end) | ||
|
||
# Exercise/Verify | ||
assert %{} = todays_disruptions() | ||
end | ||
|
||
test "returns alerts for today only" do | ||
# Setup | ||
alert_today = service_range_day() |> disruption_alert() | ||
alert_next_week = service_range_next_week() |> disruption_alert() | ||
|
||
expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now -> | ||
[alert_today, alert_next_week] | ||
end) | ||
|
||
# Exercise/Verify | ||
assert %{today: [^alert_today]} = todays_disruptions() | ||
end | ||
end | ||
|
||
defp disruption_alert(active_period) do | ||
Factories.Alerts.Alert.build(:alert, | ||
active_period: [active_period], | ||
effect: service_impacting_effects() |> Faker.Util.pick() | ||
) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: I think you could move the type from
SystemStatus.Alerts
here.(When you started on this PR, I don't think the PR introducing that type had been merged yet)