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
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Microwaveprop.Radio.Maidenhead
|
||||||
|
|
||||||
@admin_email "graham@mcintire.me"
|
@admin_email "graham@mcintire.me"
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
|
|
@ -18,6 +20,11 @@ defmodule Microwaveprop.Accounts.User do
|
||||||
field :is_admin, :boolean, default: false
|
field :is_admin, :boolean, default: false
|
||||||
field :authenticated_at, :utc_datetime, virtual: true
|
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)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -208,6 +215,114 @@ defmodule Microwaveprop.Accounts.User do
|
||||||
change(user, confirmed_at: now)
|
change(user, confirmed_at: now)
|
||||||
end
|
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 """
|
@doc """
|
||||||
Verifies the password.
|
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")
|
refute User.valid_password?(nil, "anything")
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue