Skip to content

Commit

Permalink
Shuttle page stop section (#1038)
Browse files Browse the repository at this point in the history
* chore: utility function to get stop by ID

* chore: also preload route_stops from shuttle_routes

* feat: stops section of shuttle form

* feat: ability to add and remove stops

* fix: prefill hidden values when adding stops

* test: test removing a stop

* test: test adding a stop

* feat: first pass at very basic layout

* feat: validate presence of stops before marking active
  • Loading branch information
lemald authored Nov 19, 2024
1 parent f57a04c commit 286e4e5
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 24 deletions.
52 changes: 48 additions & 4 deletions lib/arrow/shuttles.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ defmodule Arrow.Shuttles do
alias ArrowWeb.ErrorHelpers

alias Arrow.Gtfs.Route, as: GtfsRoute
alias Arrow.Gtfs.Stop, as: GtfsStop
alias Arrow.Shuttles.KML
alias Arrow.Shuttles.RouteStop
alias Arrow.Shuttles.Shape
alias Arrow.Shuttles.ShapesUpload
alias Arrow.Shuttles.ShapeUpload
alias Arrow.Shuttles.Stop

@preloads [routes: [:shape, route_stops: [:stop, :gtfs_stop]]]

@doc """
Returns the list of shapes.
Expand Down Expand Up @@ -245,7 +250,7 @@ defmodule Arrow.Shuttles do
"""
def list_shuttles do
Repo.all(Shuttle) |> Repo.preload(routes: [:shape])
Repo.all(Shuttle) |> Repo.preload(@preloads)
end

@doc """
Expand All @@ -263,7 +268,7 @@ defmodule Arrow.Shuttles do
"""
def get_shuttle!(id) do
Repo.get!(Shuttle, id) |> Repo.preload(routes: [:shape])
Repo.get!(Shuttle, id) |> Repo.preload(@preloads) |> populate_display_stop_ids()
end

@doc """
Expand All @@ -285,7 +290,7 @@ defmodule Arrow.Shuttles do
|> Repo.insert()

case created_shuttle do
{:ok, shuttle} -> {:ok, Repo.preload(shuttle, routes: [:shape])}
{:ok, shuttle} -> {:ok, shuttle |> Repo.preload(@preloads) |> populate_display_stop_ids()}
err -> err
end
end
Expand All @@ -309,7 +314,7 @@ defmodule Arrow.Shuttles do
|> Repo.update()

case updated_shuttle do
{:ok, shuttle} -> {:ok, Repo.preload(shuttle, routes: [:shape])}
{:ok, shuttle} -> {:ok, shuttle |> Repo.preload(@preloads) |> populate_display_stop_ids()}
err -> err
end
end
Expand All @@ -327,8 +332,47 @@ defmodule Arrow.Shuttles do
Shuttle.changeset(shuttle, attrs)
end

@spec populate_display_stop_ids(map()) :: map()
defp populate_display_stop_ids(shuttle) do
%{
shuttle
| routes:
Enum.map(shuttle.routes, fn route ->
%{
route
| route_stops:
Enum.map(route.route_stops, fn route_stop ->
Map.put(
route_stop,
:display_stop_id,
case route_stop do
%RouteStop{stop: %Stop{stop_id: stop_id}} -> stop_id
%RouteStop{gtfs_stop_id: gtfs_stop_id} -> gtfs_stop_id
end
)
end)
}
end)
}
end

def list_disruptable_routes do
query = from(r in GtfsRoute, where: r.type in [:light_rail, :heavy_rail])
Repo.all(query)
end

@doc """
Given a stop ID, returns either an Arrow-created stop, or a
stop from GTFS. Prefers the Arrow-created stop if both are
present.
"""
@spec stop_or_gtfs_stop_for_stop_id(String.t() | nil) :: Stop.t() | GtfsStop.t() | nil
def stop_or_gtfs_stop_for_stop_id(nil), do: nil

def stop_or_gtfs_stop_for_stop_id(id) do
case Repo.get_by(Stop, stop_id: id) do
nil -> Repo.get(GtfsStop, id)
stop -> stop
end
end
end
11 changes: 10 additions & 1 deletion lib/arrow/shuttles/route.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ defmodule Arrow.Shuttles.Route do
field :waypoint, :string
belongs_to :shuttle, Arrow.Shuttles.Shuttle
belongs_to :shape, Arrow.Shuttles.Shape
has_many :route_stop, Arrow.Shuttles.RouteStop, foreign_key: :shuttle_route_id

has_many :route_stops, Arrow.Shuttles.RouteStop,
foreign_key: :shuttle_route_id,
preload_order: [asc: :stop_sequence],
on_replace: :delete

timestamps(type: :utc_datetime)
end
Expand All @@ -20,6 +24,11 @@ defmodule Arrow.Shuttles.Route do
def changeset(route, attrs) do
route
|> cast(attrs, [:direction_id, :direction_desc, :destination, :waypoint, :suffix, :shape_id])
|> cast_assoc(:route_stops,
with: &Arrow.Shuttles.RouteStop.changeset/2,
sort_param: :route_stops_sort,
drop_param: :route_stops_drop
)
|> validate_required([:direction_id, :direction_desc, :destination])
|> assoc_constraint(:shape)
end
Expand Down
35 changes: 29 additions & 6 deletions lib/arrow/shuttles/route_stop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ defmodule Arrow.Shuttles.RouteStop do
use Ecto.Schema
import Ecto.Changeset

alias Arrow.Gtfs.Stop, as: GtfsStop
alias Arrow.Shuttles
alias Arrow.Shuttles.Stop

schema "shuttle_route_stops" do
field :direction_id, Ecto.Enum, values: [:"0", :"1"]
field :stop_sequence, :integer
field :time_to_next_stop, :decimal
field :display_stop_id, :string, virtual: true
belongs_to :shuttle_route, Arrow.Shuttles.Route
belongs_to :stop, Arrow.Shuttles.Stop
belongs_to :gtfs_stop, Arrow.Gtfs.Stop, type: :string
Expand All @@ -16,11 +21,29 @@ defmodule Arrow.Shuttles.RouteStop do

@doc false
def changeset(route_stop, attrs) do
route_stop
|> cast(attrs, [:direction_id, :stop_id, :gtfs_stop_id, :stop_sequence, :time_to_next_stop])
|> validate_required([:direction_id, :stop_sequence, :time_to_next_stop])
|> assoc_constraint(:shuttle_route)
|> assoc_constraint(:stop)
|> assoc_constraint(:gtfs_stop)
{stop_id, gtfs_stop_id, error} =
case Shuttles.stop_or_gtfs_stop_for_stop_id(
attrs["display_stop_id"] || route_stop.display_stop_id
) do
%Stop{id: id} -> {id, nil, nil}
%GtfsStop{id: id} -> {nil, id, nil}
nil -> {nil, nil, "not a valid stop ID"}
end

change =
route_stop
|> cast(attrs, [:direction_id, :stop_id, :gtfs_stop_id, :stop_sequence, :time_to_next_stop])
|> change(stop_id: stop_id)
|> change(gtfs_stop_id: gtfs_stop_id)
|> validate_required([:direction_id, :stop_sequence])
|> assoc_constraint(:shuttle_route)
|> assoc_constraint(:stop)
|> assoc_constraint(:gtfs_stop)

if is_nil(error) do
change
else
add_error(change, :display_stop_id, error)
end
end
end
18 changes: 8 additions & 10 deletions lib/arrow/shuttles/shuttle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,19 @@ defmodule Arrow.Shuttles.Shuttle do
# Placeholder validation until form is complete
status = get_field(changeset, :status)
# Set error on status field for now
fields = [:status]

case status do
:active ->
message = "can't be set to active when required fields are missing"
routes = get_assoc(changeset, :routes)

%{
enough_stops? =
routes |> Enum.map(&get_assoc(&1, :route_stops)) |> Enum.all?(&(length(&1) >= 2))

if enough_stops? do
changeset
| errors:
Enum.map(
fields,
&{&1, {message, [validation: :required]}}
),
valid?: false
}
else
add_error(changeset, :status, "must have at least two stops in each direction")
end

_ ->
changeset
Expand Down
121 changes: 118 additions & 3 deletions lib/arrow_web/live/shuttle_live/shuttle_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,51 @@ defmodule ArrowWeb.ShuttleViewLive do
</div>
</div>
</.inputs_for>
<h2>define stops</h2>
<.inputs_for :let={f_route} field={f[:routes]} as={:routes_with_stops}>
<h4>direction <%= input_value(f_route, :direction_id) %></h4>
<div class="container">
<.inputs_for :let={f_route_stop} field={f_route[:route_stops]}>
<div class="row">
<.input field={f_route_stop[:display_stop_id]} label="Stop ID" class="col-lg-7" />
<.input
field={f_route_stop[:time_to_next_stop]}
type="number"
label="Time to next stop"
class="col-lg-4"
/>
<button
type="button"
name={input_name(f_route, :route_stops_drop) <> "[]"}
value={f_route_stop.index}
phx-click={JS.dispatch("change")}
class="col-lg-1"
>
<.icon name="hero-x-mark-solid" class="h-4 w-4" />
</button>
<input
value={f_route_stop.index}
type="hidden"
name={input_name(f_route, :route_stops_sort) <> "[]"}
/>
<input
value={input_value(f_route_stop, :direction_id)}
type="hidden"
name={input_name(f_route_stop, :direction_id)}
/>
<input
value={input_value(f_route_stop, :stop_sequence)}
type="hidden"
name={input_name(f_route_stop, :stop_sequence)}
/>
</div>
</.inputs_for>
</div>
<input type="hidden" name={input_name(f_route, :route_stops_drop) <> "[]"} />
<button type="button" value={input_value(f_route, :direction_id)} phx-click="add_stop">
Add Another Stop
</button>
</.inputs_for>
<:actions>
<.button>Save Shuttle</.button>
</:actions>
Expand Down Expand Up @@ -155,7 +200,9 @@ defmodule ArrowWeb.ShuttleViewLive do
{:ok, socket}
end

def handle_event("validate", %{"shuttle" => shuttle_params}, socket) do
def handle_event("validate", params, socket) do
shuttle_params = params |> combine_params()

form =
socket.assigns.shuttle
|> Shuttles.change_shuttle(shuttle_params)
Expand All @@ -164,7 +211,9 @@ defmodule ArrowWeb.ShuttleViewLive do
{:noreply, assign(socket, form: form)}
end

def handle_event("edit", %{"shuttle" => shuttle_params}, socket) do
def handle_event("edit", params, socket) do
shuttle_params = params |> combine_params()

shuttle = Shuttles.get_shuttle!(socket.assigns.shuttle.id)

case Shuttles.update_shuttle(shuttle, shuttle_params) do
Expand All @@ -179,7 +228,9 @@ defmodule ArrowWeb.ShuttleViewLive do
end
end

def handle_event("create", %{"shuttle" => shuttle_params}, socket) do
def handle_event("create", params, socket) do
shuttle_params = params |> combine_params()

case Shuttles.create_shuttle(shuttle_params) do
{:ok, shuttle} ->
{:noreply,
Expand All @@ -191,4 +242,68 @@ defmodule ArrowWeb.ShuttleViewLive do
{:noreply, assign(socket, form: to_form(changeset))}
end
end

def handle_event("add_stop", %{"value" => direction_id}, socket) do
direction_id = String.to_existing_atom(direction_id)

socket =
update(socket, :form, fn %{source: changeset} ->
existing_routes = Ecto.Changeset.get_assoc(changeset, :routes)

new_routes =
Enum.map(existing_routes, fn route_changeset ->
update_route_changeset_with_new_stop(route_changeset, direction_id)
end)

changeset = Ecto.Changeset.put_assoc(changeset, :routes, new_routes)

to_form(changeset)
end)

{:noreply, socket}
end

defp combine_params(%{
"shuttle" => shuttle_params,
"routes_with_stops" => routes_with_stops_params
}) do
%{
shuttle_params
| "routes" =>
shuttle_params
|> Map.get("routes")
|> Map.new(fn {route_index, route} ->
route_stop_fields =
Map.take(routes_with_stops_params[route_index], [
"route_stops",
"route_stops_drop",
"route_stops_sort"
])

{route_index, Map.merge(route, route_stop_fields)}
end)
}
end

defp update_route_changeset_with_new_stop(route_changeset, direction_id) do
if Ecto.Changeset.get_field(route_changeset, :direction_id) == direction_id do
existing_stops = Ecto.Changeset.get_field(route_changeset, :route_stops)

max_stop_sequence =
existing_stops |> Enum.map(& &1.stop_sequence) |> Enum.max(fn -> 0 end)

new_route_stop = %Arrow.Shuttles.RouteStop{
direction_id: direction_id,
stop_sequence: max_stop_sequence + 1
}

Ecto.Changeset.put_assoc(
route_changeset,
:route_stops,
existing_stops ++ [new_route_stop]
)
else
route_changeset
end
end
end
Loading

0 comments on commit 286e4e5

Please sign in to comment.