- Create MaidenheadChangesetHelpers: consolidate grid validation, callsign normalization, lat/lon validation, grid/latlon derivation across 6 schemas - Create ContextHelpers: shared fetch_owned with admin bypass, safe_enqueue for Oban workers, UUID casting to replace CastError rescues - Extend LiveHelpers: add current_user/1 (removes 7 duplicate definitions), subscribe/2 (replaces 13 inline PubSub sites), assign_url_params/2 - Extract Propagation.ScoreStore (528 lines): separate file I/O and cache management from scoring logic, 13 defdelegate passthroughs - Split SubmitLive (942->475 lines): extract CSV/ADIF upload rendering into 3 function component modules (csv_upload, adif_upload, preview) - Update 16 LiveViews to use shared helpers
184 lines
5.4 KiB
Elixir
184 lines
5.4 KiB
Elixir
defmodule Microwaveprop.Rover do
|
|
@moduledoc """
|
|
Context for managing a user's fixed stations and providing the
|
|
rover-planning defaults.
|
|
|
|
All mutations are scoped to a user — `update_station/3`, `delete_station/2`,
|
|
and `toggle_selected/2` return `{:error, :not_found}` when invoked on
|
|
a station belonging to a different user.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Microwaveprop.Accounts.User
|
|
alias Microwaveprop.ContextHelpers
|
|
alias Microwaveprop.Radio.Maidenhead
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Rover.FixedStation
|
|
alias Microwaveprop.Rover.Location
|
|
|
|
@default_stations [
|
|
%{callsign: "W5LUA", grid: "EM13qc"},
|
|
%{callsign: "W5HN", grid: "EM13nf"},
|
|
%{callsign: "N5XU", grid: "EM10cl"}
|
|
]
|
|
|
|
@spec list_stations(User.t()) :: [FixedStation.t()]
|
|
def list_stations(%User{id: user_id}) do
|
|
FixedStation
|
|
|> where([s], s.user_id == ^user_id)
|
|
|> order_by([s], asc: s.position, asc: s.inserted_at)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec create_station(User.t(), map()) :: {:ok, FixedStation.t()} | {:error, Ecto.Changeset.t()}
|
|
def create_station(%User{} = user, attrs) do
|
|
attrs_with_position = put_position_default(attrs, next_position(user))
|
|
|
|
%FixedStation{user_id: user.id}
|
|
|> FixedStation.changeset(attrs_with_position)
|
|
|> Repo.insert()
|
|
|> maybe_enqueue_elevation()
|
|
end
|
|
|
|
defp put_position_default(attrs, position) when is_map(attrs) do
|
|
cond do
|
|
Map.has_key?(attrs, :position) -> attrs
|
|
Map.has_key?(attrs, "position") -> attrs
|
|
string_keyed?(attrs) -> Map.put(attrs, "position", position)
|
|
true -> Map.put(attrs, :position, position)
|
|
end
|
|
end
|
|
|
|
defp string_keyed?(attrs) do
|
|
Enum.any?(attrs, fn {k, _v} -> is_binary(k) end)
|
|
end
|
|
|
|
@spec update_station(User.t(), Ecto.UUID.t(), map()) ::
|
|
{:ok, FixedStation.t()} | {:error, :not_found | Ecto.Changeset.t()}
|
|
def update_station(%User{} = user, id, attrs) do
|
|
case ContextHelpers.fetch_owned(FixedStation, id, user) do
|
|
{:ok, station} ->
|
|
station
|
|
|> FixedStation.changeset(attrs)
|
|
|> Repo.update()
|
|
|
|
{:error, :not_found} ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@spec delete_station(User.t(), Ecto.UUID.t()) ::
|
|
{:ok, FixedStation.t()} | {:error, :not_found}
|
|
def delete_station(%User{} = user, id) do
|
|
case ContextHelpers.fetch_owned(FixedStation, id, user) do
|
|
{:ok, station} ->
|
|
Repo.delete(station)
|
|
|
|
{:error, :not_found} ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@spec toggle_selected(User.t(), Ecto.UUID.t()) ::
|
|
{:ok, FixedStation.t()} | {:error, :not_found | Ecto.Changeset.t()}
|
|
def toggle_selected(%User{} = user, id) do
|
|
case ContextHelpers.fetch_owned(FixedStation, id, user) do
|
|
{:ok, station} ->
|
|
station
|
|
|> Ecto.Changeset.change(selected: not station.selected)
|
|
|> Repo.update()
|
|
|
|
{:error, :not_found} ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Three hardcoded NTMS stations used as the anonymous-user fallback in
|
|
the rover planner. Plain maps shaped like `FixedStation`, with lat/lon
|
|
derived from each grid via `Maidenhead.to_latlon/1`.
|
|
"""
|
|
@spec default_stations() :: [map()]
|
|
def default_stations do
|
|
@default_stations
|
|
|> Enum.with_index()
|
|
|> Enum.map(fn {%{callsign: callsign, grid: grid}, index} ->
|
|
{:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
|
|
|
|
%{
|
|
callsign: callsign,
|
|
grid: grid,
|
|
lat: lat,
|
|
lon: lon,
|
|
elevation_m: nil,
|
|
selected: true,
|
|
position: index
|
|
}
|
|
end)
|
|
end
|
|
|
|
# ── Rover locations (globally visible, logged-in to mutate) ──────────
|
|
|
|
@doc """
|
|
Returns every rover location across all users, newest first.
|
|
Anonymous visitors can read this list; mutations require a User.
|
|
"""
|
|
@spec list_locations() :: [Location.t()]
|
|
def list_locations do
|
|
Location
|
|
|> order_by([l], desc: l.inserted_at)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec create_location(User.t(), map()) ::
|
|
{:ok, Location.t()} | {:error, Ecto.Changeset.t()}
|
|
def create_location(%User{} = user, attrs) do
|
|
%Location{user_id: user.id}
|
|
|> Location.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@spec update_location(User.t(), Ecto.UUID.t(), map()) ::
|
|
{:ok, Location.t()} | {:error, :not_found | Ecto.Changeset.t()}
|
|
def update_location(%User{} = user, id, attrs) do
|
|
case ContextHelpers.fetch_owned(Location, id, user) do
|
|
{:ok, location} ->
|
|
location
|
|
|> Location.changeset(attrs)
|
|
|> Repo.update()
|
|
|
|
{:error, :not_found} ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@spec delete_location(User.t(), Ecto.UUID.t()) ::
|
|
{:ok, Location.t()} | {:error, :not_found}
|
|
def delete_location(%User{} = user, id) do
|
|
case ContextHelpers.fetch_owned(Location, id, user) do
|
|
{:ok, location} -> Repo.delete(location)
|
|
{:error, :not_found} -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp next_position(%User{id: user_id}) do
|
|
query =
|
|
from s in FixedStation,
|
|
where: s.user_id == ^user_id,
|
|
select: count(s.id)
|
|
|
|
Repo.one(query) || 0
|
|
end
|
|
|
|
defp maybe_enqueue_elevation({:ok, %FixedStation{elevation_m: nil, id: id}} = result) do
|
|
_ = enqueue_station_elevation(id)
|
|
result
|
|
end
|
|
|
|
defp maybe_enqueue_elevation(other), do: other
|
|
|
|
defp enqueue_station_elevation(id) do
|
|
ContextHelpers.safe_enqueue(Microwaveprop.Workers.StationElevationWorker, %{id: id})
|
|
end
|
|
end
|