Non-admin users can now submit beacons but they are held in an unapproved state until an admin approves them. Only approved beacons appear in the public list; pending submissions are shown in a separate admin-only section on /beacons with Approve and Delete actions. The show page surfaces a pending badge and an admin-only Approve button when viewing an unapproved beacon. Also formats EIRP (mW) on the index page without scientific notation.
110 lines
3.3 KiB
Elixir
110 lines
3.3 KiB
Elixir
defmodule Microwaveprop.Beacons.Beacon do
|
|
@moduledoc """
|
|
A beacon transmitter. Frequency in MHz, coordinates as lat/lon,
|
|
grid as Maidenhead (auto-computed from lat/lon when not supplied),
|
|
power in milliwatts, height above ground in feet.
|
|
"""
|
|
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
alias Microwaveprop.Radio.Maidenhead
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
@foreign_key_type :binary_id
|
|
schema "beacons" do
|
|
field :frequency_mhz, :float
|
|
field :callsign, :string
|
|
field :grid, :string
|
|
field :lat, :float
|
|
field :lon, :float
|
|
field :power_mw, :float
|
|
field :height_ft, :float
|
|
field :on_the_air, :boolean, default: true
|
|
field :approved, :boolean, default: false
|
|
field :user_id, :binary_id
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@required_fields [:frequency_mhz, :callsign, :lat, :lon, :power_mw, :height_ft]
|
|
|
|
def changeset(beacon, attrs) do
|
|
beacon
|
|
|> cast(attrs, [:frequency_mhz, :callsign, :grid, :lat, :lon, :power_mw, :height_ft, :on_the_air])
|
|
|> update_change(:callsign, fn cs -> cs && String.upcase(String.trim(cs)) end)
|
|
|> maybe_fill_latlon()
|
|
|> maybe_fill_grid()
|
|
|> validate_required(@required_fields)
|
|
|> validate_number(:frequency_mhz, greater_than: 0)
|
|
|> validate_number(:power_mw, greater_than_or_equal_to: 0)
|
|
|> validate_number(:height_ft, greater_than_or_equal_to: 0)
|
|
|> validate_number(:lat, greater_than_or_equal_to: -90, less_than_or_equal_to: 90)
|
|
|> validate_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180)
|
|
|> validate_length(:callsign, min: 3, max: 10)
|
|
|> validate_grid_format()
|
|
end
|
|
|
|
# If grid is blank but lat/lon are valid, derive it.
|
|
defp maybe_fill_grid(changeset) do
|
|
grid = get_field(changeset, :grid)
|
|
lat = get_field(changeset, :lat)
|
|
lon = get_field(changeset, :lon)
|
|
|
|
if grid in [nil, ""] and is_number(lat) and is_number(lon) do
|
|
put_change(changeset, :grid, Maidenhead.from_latlon(lat, lon, 6))
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
# If lat/lon are blank but grid is a valid Maidenhead, derive them.
|
|
defp maybe_fill_latlon(changeset) do
|
|
grid = get_field(changeset, :grid)
|
|
lat = get_field(changeset, :lat)
|
|
lon = get_field(changeset, :lon)
|
|
|
|
if is_binary(grid) and grid != "" and (lat in [nil, ""] or lon in [nil, ""]) do
|
|
case Maidenhead.to_latlon(grid) do
|
|
{:ok, {new_lat, new_lon}} ->
|
|
changeset
|
|
|> put_change(:lat, new_lat)
|
|
|> put_change(:lon, new_lon)
|
|
|
|
:error ->
|
|
changeset
|
|
end
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
defp validate_grid_format(changeset) do
|
|
case get_field(changeset, :grid) do
|
|
nil ->
|
|
add_error(changeset, :grid, "can't be blank")
|
|
|
|
grid ->
|
|
if Maidenhead.valid?(grid) do
|
|
update_change(changeset, :grid, &normalize_grid/1)
|
|
else
|
|
add_error(changeset, :grid, "is not a valid Maidenhead grid")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Field uppercase, square digits, subsquare lowercase (standard form)
|
|
defp normalize_grid(nil), do: nil
|
|
|
|
defp normalize_grid(grid) do
|
|
grid = String.trim(grid)
|
|
len = String.length(grid)
|
|
|
|
if len >= 6 do
|
|
grid |> String.slice(0, 4) |> String.upcase() |> Kernel.<>(String.downcase(String.slice(grid, 4, len - 4)))
|
|
else
|
|
String.upcase(grid)
|
|
end
|
|
end
|
|
end
|