feat(contacts): antenna height fields feed terrain analysis
Add optional height1_ft/height2_ft to contact schema, submit form, and edit form (direct + suggest-edit paths). Heights flow through to the elevation-profile terrain analysis so clearance and diffraction are computed from the actual antenna height instead of a 10-ft default. Editing heights resets terrain_status and re-enqueues TerrainProfileWorker so the stored path analysis picks up the new geometry. Also fix mark_likely_duct/3: the pattern was grabbing the element instead of the index, so no duct ever got flagged as the likely propagation path even when extract_ducts returned a valid layer.
This commit is contained in:
parent
67d4b2eca7
commit
ca07c0288d
8 changed files with 277 additions and 19 deletions
|
|
@ -10,6 +10,7 @@ defmodule Microwaveprop.Radio do
|
|||
alias Microwaveprop.Radio.ContactEdit
|
||||
alias Microwaveprop.Radio.Maidenhead
|
||||
alias Microwaveprop.Repo
|
||||
alias Microwaveprop.Workers.ContactWeatherEnqueueWorker
|
||||
|
||||
@per_page 20
|
||||
@sortable_fields ~w(station1 station2 band mode distance_km qso_timestamp inserted_at)a
|
||||
|
|
@ -772,6 +773,8 @@ defmodule Microwaveprop.Radio do
|
|||
|> normalize_string_field("grid1")
|
||||
|> normalize_string_field("grid2")
|
||||
|> normalize_string_field("mode")
|
||||
|> normalize_integer_field("height1_ft")
|
||||
|> normalize_integer_field("height2_ft")
|
||||
end
|
||||
|
||||
defp normalize_string_field(map, key) do
|
||||
|
|
@ -782,6 +785,33 @@ defmodule Microwaveprop.Radio do
|
|||
end
|
||||
end
|
||||
|
||||
# Forms deliver integer-shaped inputs as strings. Drop the field entirely
|
||||
# on empty strings so the diff doesn't register "" != current_value.
|
||||
defp normalize_integer_field(map, key) do
|
||||
case Map.get(map, key, :not_provided) do
|
||||
:not_provided ->
|
||||
map
|
||||
|
||||
nil ->
|
||||
map
|
||||
|
||||
val when is_integer(val) ->
|
||||
map
|
||||
|
||||
"" ->
|
||||
Map.delete(map, key)
|
||||
|
||||
val when is_binary(val) ->
|
||||
case Integer.parse(String.trim(val)) do
|
||||
{int, ""} -> Map.put(map, key, int)
|
||||
_ -> map
|
||||
end
|
||||
|
||||
_ ->
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec diff_against_contact(Contact.t(), map()) :: map()
|
||||
def diff_against_contact(contact, proposed) do
|
||||
|
|
@ -807,6 +837,8 @@ defmodule Microwaveprop.Radio do
|
|||
end
|
||||
|
||||
defp current_value(contact, "qso_timestamp"), do: contact.qso_timestamp
|
||||
defp current_value(contact, "height1_ft"), do: contact.height1_ft
|
||||
defp current_value(contact, "height2_ft"), do: contact.height2_ft
|
||||
defp current_value(_contact, _key), do: nil
|
||||
|
||||
defp values_equal?(a, b) when is_binary(a) and is_binary(b) do
|
||||
|
|
@ -951,10 +983,36 @@ defmodule Microwaveprop.Radio do
|
|||
def apply_edit_to_contact(contact, proposed_changes) do
|
||||
changes = build_contact_changes(contact, proposed_changes)
|
||||
changes = maybe_recompute_positions(changes, contact, proposed_changes)
|
||||
changes = maybe_reset_terrain_for_heights(changes, proposed_changes)
|
||||
|
||||
contact
|
||||
|> Ecto.Changeset.change(changes)
|
||||
|> Repo.update!()
|
||||
updated =
|
||||
contact
|
||||
|> Ecto.Changeset.change(changes)
|
||||
|> Repo.update!()
|
||||
|
||||
maybe_enqueue_terrain_for_heights(updated, proposed_changes)
|
||||
|
||||
updated
|
||||
end
|
||||
|
||||
defp maybe_enqueue_terrain_for_heights(contact, proposed) do
|
||||
if Map.has_key?(proposed, "height1_ft") or Map.has_key?(proposed, "height2_ft") do
|
||||
ContactWeatherEnqueueWorker.enqueue_for_contact(contact, [:terrain])
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
# Antenna heights feed directly into the elevation-profile terrain
|
||||
# analysis (diffraction loss, clearance verdict). Changing them
|
||||
# invalidates the stored terrain_profile, so reset the status to
|
||||
# :pending and let TerrainProfileWorker re-run.
|
||||
defp maybe_reset_terrain_for_heights(changes, proposed) do
|
||||
if Map.has_key?(proposed, "height1_ft") or Map.has_key?(proposed, "height2_ft") do
|
||||
Map.put(changes, :terrain_status, :pending)
|
||||
else
|
||||
changes
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_recompute_positions(changes, contact, proposed) do
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@ defmodule Microwaveprop.Radio.Contact do
|
|||
field :submitter_email, :string
|
||||
field :flagged_invalid, :boolean, default: false
|
||||
|
||||
# Antenna height above ground level (feet). Optional — when set, the
|
||||
# elevation profile and terrain analysis use the actual heights
|
||||
# instead of a default 10 ft.
|
||||
field :height1_ft, :integer
|
||||
field :height2_ft, :integer
|
||||
|
||||
belongs_to :user, User
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
|
|
@ -68,7 +74,7 @@ defmodule Microwaveprop.Radio.Contact do
|
|||
@type t :: %__MODULE__{}
|
||||
|
||||
@required_fields ~w(station1 station2 qso_timestamp band)a
|
||||
@optional_fields ~w(grid1 grid2 pos1 pos2 distance_km user_id mode user_declared_prop_mode)a
|
||||
@optional_fields ~w(grid1 grid2 pos1 pos2 distance_km user_id mode user_declared_prop_mode height1_ft height2_ft)a
|
||||
|
||||
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
||||
def changeset(contact, attrs) do
|
||||
|
|
@ -77,7 +83,7 @@ defmodule Microwaveprop.Radio.Contact do
|
|||
|> validate_required(@required_fields)
|
||||
end
|
||||
|
||||
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_declared_prop_mode)a
|
||||
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_declared_prop_mode height1_ft height2_ft)a
|
||||
@submission_required ~w(station1 station2 qso_timestamp band grid1 grid2)a
|
||||
@allowed_modes ~w(CW SSB FM FT8 FT4 Q65)
|
||||
# Keep in sync with Microwaveprop.Propagation.BandConfig.all_bands/0 and
|
||||
|
|
@ -108,6 +114,14 @@ defmodule Microwaveprop.Radio.Contact do
|
|||
|> validate_length(:submitter_email, max: 254)
|
||||
|> validate_length(:station1, max: 20)
|
||||
|> validate_length(:station2, max: 20)
|
||||
|> validate_height(:height1_ft)
|
||||
|> validate_height(:height2_ft)
|
||||
end
|
||||
|
||||
# Sanity bounds on AGL antenna height. Negative heights and anything
|
||||
# taller than ~1000 ft are almost certainly data-entry errors.
|
||||
defp validate_height(changeset, field) do
|
||||
validate_number(changeset, field, greater_than_or_equal_to: 0, less_than_or_equal_to: 1000)
|
||||
end
|
||||
|
||||
# Treat empty / whitespace-only mode strings as "not provided" so the column
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Microwaveprop.Radio.ContactEdit do
|
|||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@editable_fields ~w(station1 station2 grid1 grid2 band mode qso_timestamp)
|
||||
@editable_fields ~w(station1 station2 grid1 grid2 band mode qso_timestamp height1_ft height2_ft)
|
||||
|
||||
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
||||
def changeset(edit, attrs) do
|
||||
|
|
@ -118,8 +118,21 @@ defmodule Microwaveprop.Radio.ContactEdit do
|
|||
end
|
||||
end
|
||||
|
||||
defp validate_field_value("height1_ft", cs), do: validate_height_value(cs, "height1_ft")
|
||||
defp validate_field_value("height2_ft", cs), do: validate_height_value(cs, "height2_ft")
|
||||
|
||||
defp validate_field_value(_, changeset), do: changeset
|
||||
|
||||
defp validate_height_value(changeset, field) do
|
||||
val = get_field(changeset, :proposed_changes)[field]
|
||||
|
||||
if is_integer(val) and val >= 0 and val <= 1000 do
|
||||
changeset
|
||||
else
|
||||
add_error(changeset, :proposed_changes, "#{field} must be an integer between 0 and 1000")
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_callsign_value(changeset, field) do
|
||||
val = get_field(changeset, :proposed_changes)[field] || ""
|
||||
val = val |> String.trim() |> String.upcase()
|
||||
|
|
|
|||
|
|
@ -262,7 +262,9 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
"band" => if(contact.band, do: Decimal.to_string(contact.band), else: ""),
|
||||
"mode" => contact.mode,
|
||||
"qso_timestamp" =>
|
||||
if(contact.qso_timestamp, do: Calendar.strftime(contact.qso_timestamp, "%Y-%m-%d %H:%M"), else: "")
|
||||
if(contact.qso_timestamp, do: Calendar.strftime(contact.qso_timestamp, "%Y-%m-%d %H:%M"), else: ""),
|
||||
"height1_ft" => contact.height1_ft,
|
||||
"height2_ft" => contact.height2_ft
|
||||
}
|
||||
|
||||
{:noreply, assign(socket, editing: true, edit_form: to_form(form_data, as: "edit"))}
|
||||
|
|
@ -836,7 +838,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
{if direct_edit,
|
||||
do: "Change any fields below. Changes will be applied immediately.",
|
||||
do: "Change any fields below. Changes will be submitted for review.",
|
||||
else: "Change any fields below. Only fields you modify will be submitted for review."}
|
||||
</p>
|
||||
<.form
|
||||
|
|
@ -846,11 +848,27 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
phx-submit="submit_edit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<.input field={@edit_form[:station1]} type="text" label="Station 1" />
|
||||
<.input field={@edit_form[:grid1]} type="text" label="Grid 1" />
|
||||
<.input
|
||||
field={@edit_form[:height1_ft]}
|
||||
type="number"
|
||||
label="Height 1 (ft AGL)"
|
||||
min="0"
|
||||
max="1000"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
<.input field={@edit_form[:station2]} type="text" label="Station 2" />
|
||||
<.input field={@edit_form[:grid2]} type="text" label="Grid 2" />
|
||||
<.input
|
||||
field={@edit_form[:height2_ft]}
|
||||
type="number"
|
||||
label="Height 2 (ft AGL)"
|
||||
min="0"
|
||||
max="1000"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<.input field={@edit_form[:band]} type="select" label="Band" options={@band_options} />
|
||||
|
|
@ -1647,16 +1665,18 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
{:ok, profile} <- safe_fetch_elevation(lat1, lon1, lat2, lon2) do
|
||||
freq_ghz = band_to_ghz(contact.band)
|
||||
dist_km = haversine_km(lat1, lon1, lat2, lon2)
|
||||
# Assume both stations are at 10 ft (3.048 m) AGL so the path
|
||||
# analysis doesn't pretend the antennas are sitting on the dirt.
|
||||
ant_ht_m = 3.048
|
||||
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz, ant_ht_a: ant_ht_m, ant_ht_b: ant_ht_m)
|
||||
# Default to 10 ft (3.048 m) AGL when the operator didn't record an
|
||||
# antenna height — better than pretending the antenna is on the dirt.
|
||||
default_ht_m = 3.048
|
||||
ant_ht_a_m = ft_to_m(contact.height1_ft) || default_ht_m
|
||||
ant_ht_b_m = ft_to_m(contact.height2_ft) || default_ht_m
|
||||
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz, ant_ht_a: ant_ht_a_m, ant_ht_b: ant_ht_b_m)
|
||||
|
||||
fwd_az = initial_bearing(lat1, lon1, lat2, lon2)
|
||||
rev_az = initial_bearing(lat2, lon2, lat1, lon1)
|
||||
|
||||
tx_elev = hd(profile).elev + ant_ht_m
|
||||
rx_elev = List.last(profile).elev + ant_ht_m
|
||||
tx_elev = hd(profile).elev + ant_ht_a_m
|
||||
rx_elev = List.last(profile).elev + ant_ht_b_m
|
||||
fwd_el = elevation_angle(tx_elev, rx_elev, dist_km * 1000)
|
||||
rev_el = elevation_angle(rx_elev, tx_elev, dist_km * 1000)
|
||||
|
||||
|
|
@ -1804,7 +1824,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
defp mark_likely_duct(ducts, tx_elev, rx_elev) do
|
||||
avg_elev = (tx_elev + rx_elev) / 2
|
||||
|
||||
{likely_idx, _} =
|
||||
{_, likely_idx} =
|
||||
ducts
|
||||
|> Enum.with_index()
|
||||
|> Enum.min_by(fn {d, _idx} ->
|
||||
|
|
@ -1838,6 +1858,9 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
|> Kernel./(1000)
|
||||
end
|
||||
|
||||
defp ft_to_m(nil), do: nil
|
||||
defp ft_to_m(ft) when is_integer(ft), do: ft * 0.3048
|
||||
|
||||
defp haversine_km(lat1, lon1, lat2, lon2) do
|
||||
dlat = deg_to_rad(lat2 - lat1)
|
||||
dlon = deg_to_rad(lon2 - lon1)
|
||||
|
|
@ -1937,7 +1960,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
summary =
|
||||
build_summary(terrain, elevation_profile, hrrr, soundings, dist_km, band_mhz, contact)
|
||||
|
||||
details = build_details(hrrr, hrrr_path, soundings, elevation_profile, band_mhz)
|
||||
details = build_details(hrrr, hrrr_path, soundings, elevation_profile, band_mhz, contact)
|
||||
|
||||
%{
|
||||
summary: summary,
|
||||
|
|
@ -2137,9 +2160,10 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
|
||||
defp band_summary(_, _), do: nil
|
||||
|
||||
defp build_details(hrrr, hrrr_path, soundings, elevation_profile, _band_mhz) do
|
||||
defp build_details(hrrr, hrrr_path, soundings, elevation_profile, _band_mhz, contact) do
|
||||
Enum.reject(
|
||||
[
|
||||
antenna_heights_detail(contact),
|
||||
refractivity_detail(hrrr),
|
||||
boundary_layer_detail(hrrr),
|
||||
path_duct_count_detail(hrrr_path),
|
||||
|
|
@ -2150,6 +2174,21 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
)
|
||||
end
|
||||
|
||||
defp antenna_heights_detail(%{height1_ft: h1, height2_ft: h2} = contact) when is_integer(h1) or is_integer(h2) do
|
||||
s1 = contact.station1 || "Station 1"
|
||||
s2 = contact.station2 || "Station 2"
|
||||
|
||||
parts =
|
||||
Enum.reject(
|
||||
[if(is_integer(h1), do: "#{s1} @ #{h1} ft AGL"), if(is_integer(h2), do: "#{s2} @ #{h2} ft AGL")],
|
||||
&is_nil/1
|
||||
)
|
||||
|
||||
"Antenna heights: #{Enum.join(parts, ", ")}."
|
||||
end
|
||||
|
||||
defp antenna_heights_detail(_), do: nil
|
||||
|
||||
defp path_duct_count_detail(hrrr_path) when is_list(hrrr_path) and hrrr_path != [] do
|
||||
total = length(hrrr_path)
|
||||
ducting = Enum.count(hrrr_path, &(&1 && &1.ducting_detected))
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
defp single_contact_form(assigns) do
|
||||
~H"""
|
||||
<.form for={@form} id="contact-form" phx-change="validate" phx-submit="save" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<.input field={@form[:station1]} type="text" label="Station 1" placeholder="W5XD" required />
|
||||
<.input
|
||||
field={@form[:grid1]}
|
||||
|
|
@ -334,6 +334,14 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
phx-debounce="blur"
|
||||
required
|
||||
/>
|
||||
<.input
|
||||
field={@form[:height1_ft]}
|
||||
type="number"
|
||||
label="Height 1 (ft AGL)"
|
||||
min="0"
|
||||
max="1000"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
<.input field={@form[:station2]} type="text" label="Station 2" placeholder="K5TR" required />
|
||||
<.input
|
||||
field={@form[:grid2]}
|
||||
|
|
@ -343,10 +351,19 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
phx-debounce="blur"
|
||||
required
|
||||
/>
|
||||
<.input
|
||||
field={@form[:height2_ft]}
|
||||
type="number"
|
||||
label="Height 2 (ft AGL)"
|
||||
min="0"
|
||||
max="1000"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-base-content/60 -mt-2">
|
||||
Be as specific as possible with grid squares (8 characters preferred, e.g. EM12kp37).
|
||||
Antenna heights are optional but improve terrain-clearance analysis.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
defmodule Microwaveprop.Repo.Migrations.AddHeightFtToContacts do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:contacts) do
|
||||
add :height1_ft, :integer
|
||||
add :height2_ft, :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,6 +5,30 @@ defmodule Microwaveprop.Radio.ContactEditTest do
|
|||
alias Microwaveprop.Radio
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Radio.ContactEdit
|
||||
alias Microwaveprop.Terrain.ElevationClient
|
||||
alias Microwaveprop.Weather.HrrrClient
|
||||
alias Microwaveprop.Weather.IemClient
|
||||
|
||||
setup do
|
||||
# Height edits auto-enqueue TerrainProfileWorker (which Oban runs
|
||||
# inline in tests). Stub the external data sources so the job can
|
||||
# complete without hitting the network.
|
||||
Req.Test.stub(ElevationClient, fn conn ->
|
||||
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||
lat_count = params["latitude"] |> String.split(",") |> length()
|
||||
Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
|
||||
end)
|
||||
|
||||
Req.Test.stub(HrrrClient, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 404, "not found")
|
||||
end)
|
||||
|
||||
Req.Test.stub(IemClient, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 404, "not found")
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@contact_attrs %{
|
||||
station1: "W5ISP",
|
||||
|
|
@ -120,6 +144,39 @@ defmodule Microwaveprop.Radio.ContactEditTest do
|
|||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "accepts integer height1_ft and height2_ft", %{contact: contact, user: user} do
|
||||
changeset =
|
||||
ContactEdit.changeset(%ContactEdit{}, %{
|
||||
contact_id: contact.id,
|
||||
user_id: user.id,
|
||||
proposed_changes: %{"height1_ft" => 25, "height2_ft" => 60}
|
||||
})
|
||||
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "rejects non-integer height1_ft", %{contact: contact, user: user} do
|
||||
changeset =
|
||||
ContactEdit.changeset(%ContactEdit{}, %{
|
||||
contact_id: contact.id,
|
||||
user_id: user.id,
|
||||
proposed_changes: %{"height1_ft" => "tall"}
|
||||
})
|
||||
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "rejects out-of-range height2_ft", %{contact: contact, user: user} do
|
||||
changeset =
|
||||
ContactEdit.changeset(%ContactEdit{}, %{
|
||||
contact_id: contact.id,
|
||||
user_id: user.id,
|
||||
proposed_changes: %{"height2_ft" => 9999}
|
||||
})
|
||||
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "validates mode value", %{contact: contact, user: user} do
|
||||
changeset =
|
||||
ContactEdit.changeset(%ContactEdit{}, %{
|
||||
|
|
@ -301,6 +358,26 @@ defmodule Microwaveprop.Radio.ContactEditTest do
|
|||
assert {:error, :no_changes} =
|
||||
Radio.apply_owner_edit(contact, user, %{"station1" => contact.station1})
|
||||
end
|
||||
|
||||
test "owner can set height1_ft and height2_ft", %{user: user} do
|
||||
contact = owned_contact(user)
|
||||
|
||||
assert {:ok, updated} =
|
||||
Radio.apply_owner_edit(contact, user, %{"height1_ft" => 20, "height2_ft" => 75})
|
||||
|
||||
assert updated.height1_ft == 20
|
||||
assert updated.height2_ft == 75
|
||||
end
|
||||
|
||||
test "height change resets terrain_status so terrain is re-analyzed", %{user: user} do
|
||||
contact = owned_contact(user)
|
||||
contact = contact |> Ecto.Changeset.change(terrain_status: :complete) |> Repo.update!()
|
||||
|
||||
{:ok, updated} = Radio.apply_owner_edit(contact, user, %{"height1_ft" => 30})
|
||||
|
||||
assert updated.height1_ft == 30
|
||||
assert updated.terrain_status == :pending
|
||||
end
|
||||
end
|
||||
|
||||
describe "Radio.owner?/2" do
|
||||
|
|
|
|||
|
|
@ -42,5 +42,35 @@ defmodule Microwaveprop.Radio.ContactTest do
|
|||
assert Decimal.equal?(contact.band, Decimal.new("144.2"))
|
||||
assert Decimal.equal?(contact.distance_km, Decimal.new("623.4"))
|
||||
end
|
||||
|
||||
test "accepts optional height1_ft and height2_ft" do
|
||||
attrs = Map.merge(@valid_attrs, %{height1_ft: 25, height2_ft: 60})
|
||||
changeset = Contact.changeset(%Contact{}, attrs)
|
||||
assert changeset.valid?
|
||||
assert {:ok, contact} = Repo.insert(changeset)
|
||||
assert contact.height1_ft == 25
|
||||
assert contact.height2_ft == 60
|
||||
end
|
||||
end
|
||||
|
||||
describe "submission_changeset/2" do
|
||||
@submission_attrs %{
|
||||
"station1" => "W1AW",
|
||||
"station2" => "K3LR",
|
||||
"qso_timestamp" => "2026-03-28T12:00:00Z",
|
||||
"grid1" => "FN31pr",
|
||||
"grid2" => "EN91jm",
|
||||
"mode" => "SSB",
|
||||
"band" => Decimal.new("144"),
|
||||
"submitter_email" => "test@example.com"
|
||||
}
|
||||
|
||||
test "accepts height1_ft and height2_ft" do
|
||||
attrs = Map.merge(@submission_attrs, %{"height1_ft" => 10, "height2_ft" => 50})
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert changeset.valid?, inspect(errors_on(changeset))
|
||||
assert Ecto.Changeset.get_field(changeset, :height1_ft) == 10
|
||||
assert Ecto.Changeset.get_field(changeset, :height2_ft) == 50
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue