feat(accounts): add home QTH fields to User
This commit is contained in:
parent
cf7dec79cf
commit
5932a5e4f9
4 changed files with 182 additions and 0 deletions
|
|
@ -4,6 +4,8 @@ defmodule Microwaveprop.Accounts.User do
|
|||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Microwaveprop.Radio.Maidenhead
|
||||
|
||||
@admin_email "graham@mcintire.me"
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
|
|
@ -18,6 +20,11 @@ defmodule Microwaveprop.Accounts.User do
|
|||
field :is_admin, :boolean, default: false
|
||||
field :authenticated_at, :utc_datetime, virtual: true
|
||||
|
||||
field :home_grid, :string
|
||||
field :home_lat, :float
|
||||
field :home_lon, :float
|
||||
field :home_elevation_m, :integer
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
|
|
@ -208,6 +215,114 @@ defmodule Microwaveprop.Accounts.User do
|
|||
change(user, confirmed_at: now)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for the rover-page "home QTH" fields.
|
||||
|
||||
Accepts either a Maidenhead grid or an explicit lat/lon. When only the
|
||||
grid is given, lat/lon are derived from its centre; when only lat/lon
|
||||
are given, the grid is left untouched. An empty `home_grid` paired
|
||||
with explicit nil lat/lon clears all home fields (used by the "clear
|
||||
home" affordance on /rover).
|
||||
"""
|
||||
@spec change_home_qth(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
||||
def change_home_qth(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:home_grid, :home_lat, :home_lon, :home_elevation_m])
|
||||
|> normalize_home_grid()
|
||||
|> validate_home_grid_format()
|
||||
|> derive_home_latlon_from_grid()
|
||||
|> validate_home_latlon_range()
|
||||
|> require_home_qth_pair()
|
||||
end
|
||||
|
||||
defp normalize_home_grid(changeset) do
|
||||
case get_change(changeset, :home_grid) do
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
"" ->
|
||||
put_change(changeset, :home_grid, nil)
|
||||
|
||||
grid when is_binary(grid) ->
|
||||
put_change(changeset, :home_grid, normalize_grid(grid))
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_grid(grid) do
|
||||
trimmed = String.trim(grid)
|
||||
|
||||
case String.length(trimmed) do
|
||||
4 -> String.upcase(trimmed)
|
||||
6 -> String.upcase(String.slice(trimmed, 0, 4)) <> String.downcase(String.slice(trimmed, 4, 2))
|
||||
_ -> trimmed
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_home_grid_format(changeset) do
|
||||
case get_field(changeset, :home_grid) do
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
grid ->
|
||||
if Maidenhead.valid?(grid),
|
||||
do: changeset,
|
||||
else: add_error(changeset, :home_grid, "must be a 4 or 6 character Maidenhead locator")
|
||||
end
|
||||
end
|
||||
|
||||
defp derive_home_latlon_from_grid(changeset) do
|
||||
cond do
|
||||
not changeset.valid? ->
|
||||
changeset
|
||||
|
||||
get_change(changeset, :home_lat) || get_change(changeset, :home_lon) ->
|
||||
changeset
|
||||
|
||||
grid = get_change(changeset, :home_grid) ->
|
||||
case Maidenhead.to_latlon(grid) do
|
||||
{:ok, {lat, lon}} ->
|
||||
changeset
|
||||
|> put_change(:home_lat, lat)
|
||||
|> put_change(:home_lon, lon)
|
||||
|
||||
:error ->
|
||||
changeset
|
||||
end
|
||||
|
||||
true ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_home_latlon_range(changeset) do
|
||||
changeset
|
||||
|> validate_number(:home_lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0)
|
||||
|> validate_number(:home_lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
|
||||
end
|
||||
|
||||
defp require_home_qth_pair(changeset) do
|
||||
if home_qth_satisfied?(changeset),
|
||||
do: changeset,
|
||||
else: add_error(changeset, :home_grid, "must provide a grid or lat/lon")
|
||||
end
|
||||
|
||||
defp home_qth_satisfied?(changeset) do
|
||||
grid = get_field(changeset, :home_grid)
|
||||
lat = get_field(changeset, :home_lat)
|
||||
lon = get_field(changeset, :home_lon)
|
||||
|
||||
cond do
|
||||
is_binary(grid) and grid != "" -> true
|
||||
is_number(lat) and is_number(lon) -> true
|
||||
is_nil(grid) and is_nil(lat) and is_nil(lon) -> any_home_change?(changeset)
|
||||
true -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp any_home_change?(changeset) do
|
||||
Enum.any?([:home_grid, :home_lat, :home_lon], &Map.has_key?(changeset.changes, &1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies the password.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
defmodule Microwaveprop.Repo.Migrations.AddHomeQthToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add :home_grid, :string
|
||||
add :home_lat, :float
|
||||
add :home_lon, :float
|
||||
add :home_elevation_m, :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
BIN
scripts/__pycache__/recalibrate_algo.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/recalibrate_algo.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -442,4 +442,59 @@ defmodule Microwaveprop.Accounts.UserTest do
|
|||
refute User.valid_password?(nil, "anything")
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_home_qth/2" do
|
||||
test "derives lat/lon from a 6-char grid" do
|
||||
changeset = User.change_home_qth(%User{}, %{"home_grid" => "EM13qc"})
|
||||
assert changeset.valid?
|
||||
lat = Ecto.Changeset.get_change(changeset, :home_lat)
|
||||
lon = Ecto.Changeset.get_change(changeset, :home_lon)
|
||||
assert_in_delta lat, 33.10, 0.05
|
||||
assert_in_delta lon, -96.625, 0.05
|
||||
assert Ecto.Changeset.get_change(changeset, :home_grid) == "EM13qc"
|
||||
end
|
||||
|
||||
test "accepts explicit lat/lon without grid" do
|
||||
changeset = User.change_home_qth(%User{}, %{"home_lat" => 33.0, "home_lon" => -96.7})
|
||||
assert changeset.valid?
|
||||
assert Ecto.Changeset.get_change(changeset, :home_lat) == 33.0
|
||||
assert Ecto.Changeset.get_change(changeset, :home_lon) == -96.7
|
||||
end
|
||||
|
||||
test "rejects invalid grids" do
|
||||
changeset = User.change_home_qth(%User{}, %{"home_grid" => "ZZ99zz"})
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset)[:home_grid]
|
||||
end
|
||||
|
||||
test "requires either grid or lat/lon" do
|
||||
changeset = User.change_home_qth(%User{}, %{})
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset)[:home_grid]
|
||||
end
|
||||
|
||||
test "rejects out-of-range lat/lon" do
|
||||
changeset = User.change_home_qth(%User{}, %{"home_lat" => 100.0, "home_lon" => 0.0})
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset)[:home_lat]
|
||||
end
|
||||
|
||||
test "uppercase Field + lowercase subsquare normalisation" do
|
||||
changeset = User.change_home_qth(%User{}, %{"home_grid" => "em13QC"})
|
||||
assert changeset.valid?
|
||||
assert Ecto.Changeset.get_change(changeset, :home_grid) == "EM13qc"
|
||||
end
|
||||
|
||||
test "clears all home fields when home_grid is an empty string" do
|
||||
user = %User{home_grid: "EM13qc", home_lat: 33.1, home_lon: -96.8, home_elevation_m: 200}
|
||||
|
||||
changeset =
|
||||
User.change_home_qth(user, %{"home_grid" => "", "home_lat" => nil, "home_lon" => nil})
|
||||
|
||||
assert changeset.valid?
|
||||
assert Ecto.Changeset.get_change(changeset, :home_grid) == nil
|
||||
assert Ecto.Changeset.get_change(changeset, :home_lat) == nil
|
||||
assert Ecto.Changeset.get_change(changeset, :home_lon) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue