Rename qsos to contacts throughout codebase, keep DB table name
Rename all modules, functions, variables, routes, and UI text from qso/qsos to contact/contacts. Database table stays as "qsos" to avoid migration. Add /qsos -> /contacts redirects for old URLs.
This commit is contained in:
parent
bc529af392
commit
254e64dedc
26 changed files with 620 additions and 609 deletions
|
|
@ -51,7 +51,7 @@ config :microwaveprop, Oban,
|
|||
{Oban.Plugins.Cron,
|
||||
crontab: [
|
||||
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},
|
||||
{"*/30 * * * *", Microwaveprop.Workers.QsoWeatherEnqueueWorker},
|
||||
{"*/30 * * * *", Microwaveprop.Workers.ContactWeatherEnqueueWorker},
|
||||
{"*/5 * * * *", Microwaveprop.Commercial.PollWorker},
|
||||
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker}
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ config :microwaveprop, Oban,
|
|||
crontab: [
|
||||
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker},
|
||||
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},
|
||||
{"*/30 * * * *", Microwaveprop.Workers.QsoWeatherEnqueueWorker}
|
||||
{"*/30 * * * *", Microwaveprop.Workers.ContactWeatherEnqueueWorker}
|
||||
]}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@ defmodule Microwaveprop.Radio do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Radio.Maidenhead
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
@per_page 20
|
||||
@sortable_fields ~w(station1 station2 band mode distance_km qso_timestamp inserted_at)a
|
||||
|
||||
def list_qsos(opts \\ []) do
|
||||
def list_contacts(opts \\ []) do
|
||||
page = max(Keyword.get(opts, :page, 1), 1)
|
||||
offset = (page - 1) * @per_page
|
||||
search = Keyword.get(opts, :search)
|
||||
|
||||
{sort_field, sort_dir} = sort_opts(opts)
|
||||
|
||||
base_query = maybe_search(Qso, search)
|
||||
base_query = maybe_search(Contact, search)
|
||||
|
||||
total_entries = Repo.aggregate(base_query, :count)
|
||||
total_pages = max(ceil(total_entries / @per_page), 1)
|
||||
|
|
@ -39,26 +39,26 @@ defmodule Microwaveprop.Radio do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Groups reciprocal QSOs (same station pair swapped, same band, same timestamp).
|
||||
Returns a list of `{primary_qso, reciprocals}` tuples where reciprocals is
|
||||
a (possibly empty) list of matching QSOs.
|
||||
Groups reciprocal contacts (same station pair swapped, same band, same timestamp).
|
||||
Returns a list of `{primary, reciprocals}` tuples where reciprocals is
|
||||
a (possibly empty) list of matching contacts.
|
||||
"""
|
||||
def group_reciprocals(qsos) do
|
||||
def group_reciprocals(contacts) do
|
||||
{groups, _seen_ids} =
|
||||
Enum.reduce(qsos, {[], MapSet.new()}, fn qso, {groups, seen} ->
|
||||
if MapSet.member?(seen, qso.id) do
|
||||
Enum.reduce(contacts, {[], MapSet.new()}, fn contact, {groups, seen} ->
|
||||
if MapSet.member?(seen, contact.id) do
|
||||
{groups, seen}
|
||||
else
|
||||
reciprocals =
|
||||
Enum.filter(qsos, fn other ->
|
||||
other.id != qso.id and
|
||||
Enum.filter(contacts, fn other ->
|
||||
other.id != contact.id and
|
||||
not MapSet.member?(seen, other.id) and
|
||||
other.band == qso.band and
|
||||
reciprocal_pair?(qso, other)
|
||||
other.band == contact.band and
|
||||
reciprocal_pair?(contact, other)
|
||||
end)
|
||||
|
||||
new_seen = Enum.reduce(reciprocals, MapSet.put(seen, qso.id), &MapSet.put(&2, &1.id))
|
||||
{[{qso, reciprocals} | groups], new_seen}
|
||||
new_seen = Enum.reduce(reciprocals, MapSet.put(seen, contact.id), &MapSet.put(&2, &1.id))
|
||||
{[{contact, reciprocals} | groups], new_seen}
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
@ -97,75 +97,75 @@ defmodule Microwaveprop.Radio do
|
|||
{field, dir}
|
||||
end
|
||||
|
||||
def unprocessed_qsos(limit \\ 500) do
|
||||
Qso
|
||||
def unprocessed_contacts(limit \\ 500) do
|
||||
Contact
|
||||
|> where([q], q.weather_queued == false and not is_nil(q.pos1))
|
||||
|> order_by([q], asc: q.qso_timestamp)
|
||||
|> limit(^limit)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def mark_weather_queued!(qso_ids) do
|
||||
Qso
|
||||
|> where([q], q.id in ^qso_ids)
|
||||
def mark_weather_queued!(ids) do
|
||||
Contact
|
||||
|> where([q], q.id in ^ids)
|
||||
|> Repo.update_all(set: [weather_queued: true])
|
||||
end
|
||||
|
||||
def unprocessed_hrrr_qsos(limit \\ 500) do
|
||||
Qso
|
||||
def unprocessed_hrrr_contacts(limit \\ 500) do
|
||||
Contact
|
||||
|> where([q], q.hrrr_queued == false and not is_nil(q.pos1))
|
||||
|> order_by([q], asc: q.qso_timestamp)
|
||||
|> limit(^limit)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def mark_hrrr_queued!(qso_ids) do
|
||||
Qso
|
||||
|> where([q], q.id in ^qso_ids)
|
||||
def mark_hrrr_queued!(ids) do
|
||||
Contact
|
||||
|> where([q], q.id in ^ids)
|
||||
|> Repo.update_all(set: [hrrr_queued: true])
|
||||
end
|
||||
|
||||
def unprocessed_terrain_qsos(limit \\ 500) do
|
||||
Qso
|
||||
def unprocessed_terrain_contacts(limit \\ 500) do
|
||||
Contact
|
||||
|> where([q], q.terrain_queued == false and not is_nil(q.pos1) and not is_nil(q.pos2))
|
||||
|> order_by([q], asc: q.qso_timestamp)
|
||||
|> limit(^limit)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def mark_terrain_queued!(qso_ids) do
|
||||
Qso
|
||||
|> where([q], q.id in ^qso_ids)
|
||||
def mark_terrain_queued!(ids) do
|
||||
Contact
|
||||
|> where([q], q.id in ^ids)
|
||||
|> Repo.update_all(set: [terrain_queued: true])
|
||||
end
|
||||
|
||||
def unprocessed_iemre_qsos(limit \\ 500) do
|
||||
Qso
|
||||
def unprocessed_iemre_contacts(limit \\ 500) do
|
||||
Contact
|
||||
|> where([q], q.iemre_queued == false and not is_nil(q.pos1))
|
||||
|> order_by([q], asc: q.qso_timestamp)
|
||||
|> limit(^limit)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def mark_iemre_queued!(qso_ids) do
|
||||
Qso
|
||||
|> where([q], q.id in ^qso_ids)
|
||||
def mark_iemre_queued!(ids) do
|
||||
Contact
|
||||
|> where([q], q.id in ^ids)
|
||||
|> Repo.update_all(set: [iemre_queued: true])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of {lat, lon} points along the QSO path: pos1, midpoint, pos2.
|
||||
Returns a list of {lat, lon} points along the contact path: pos1, midpoint, pos2.
|
||||
Returns [pos1] if pos2 is nil, or [] if pos1 is nil.
|
||||
"""
|
||||
def qso_path_points(%{pos1: nil}), do: []
|
||||
def contact_path_points(%{pos1: nil}), do: []
|
||||
|
||||
def qso_path_points(%{pos1: pos1, pos2: nil}) do
|
||||
def contact_path_points(%{pos1: pos1, pos2: nil}) do
|
||||
lat = pos1["lat"]
|
||||
lon = pos1["lon"] || pos1["lng"]
|
||||
if lat && lon, do: [{lat, lon}], else: []
|
||||
end
|
||||
|
||||
def qso_path_points(%{pos1: pos1, pos2: pos2}) do
|
||||
def contact_path_points(%{pos1: pos1, pos2: pos2}) do
|
||||
lat1 = pos1["lat"]
|
||||
lon1 = pos1["lon"] || pos1["lng"]
|
||||
lat2 = pos2["lat"]
|
||||
|
|
@ -195,19 +195,19 @@ defmodule Microwaveprop.Radio do
|
|||
2 * @earth_radius_km * :math.asin(:math.sqrt(a))
|
||||
end
|
||||
|
||||
def backfill_distances(qsos) do
|
||||
Enum.each(qsos, fn qso ->
|
||||
if is_nil(qso.distance_km) && qso.pos1 && qso.pos2 do
|
||||
lat1 = qso.pos1["lat"]
|
||||
lon1 = qso.pos1["lon"] || qso.pos1["lng"]
|
||||
lat2 = qso.pos2["lat"]
|
||||
lon2 = qso.pos2["lon"] || qso.pos2["lng"]
|
||||
def backfill_distances(contacts) do
|
||||
Enum.each(contacts, fn contact ->
|
||||
if is_nil(contact.distance_km) && contact.pos1 && contact.pos2 do
|
||||
lat1 = contact.pos1["lat"]
|
||||
lon1 = contact.pos1["lon"] || contact.pos1["lng"]
|
||||
lat2 = contact.pos2["lat"]
|
||||
lon2 = contact.pos2["lon"] || contact.pos2["lng"]
|
||||
|
||||
if lat1 && lon1 && lat2 && lon2 do
|
||||
dist = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
|
||||
|
||||
Qso
|
||||
|> where([q], q.id == ^qso.id)
|
||||
Contact
|
||||
|> where([q], q.id == ^contact.id)
|
||||
|> Repo.update_all(set: [distance_km: dist])
|
||||
end
|
||||
end
|
||||
|
|
@ -216,16 +216,16 @@ defmodule Microwaveprop.Radio do
|
|||
|
||||
defp deg_to_rad(deg), do: deg * :math.pi() / 180
|
||||
|
||||
def get_qso!(id) do
|
||||
Repo.get!(Qso, id)
|
||||
def get_contact!(id) do
|
||||
Repo.get!(Contact, id)
|
||||
end
|
||||
|
||||
def change_qso(qso, attrs \\ %{}) do
|
||||
Qso.submission_changeset(qso, attrs)
|
||||
def change_contact(contact, attrs \\ %{}) do
|
||||
Contact.submission_changeset(contact, attrs)
|
||||
end
|
||||
|
||||
def create_qso(attrs) do
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
def create_contact(attrs) do
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
|
||||
if changeset.valid? do
|
||||
grid1 = Ecto.Changeset.get_change(changeset, :grid1)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Microwaveprop.Radio.Qso do
|
||||
defmodule Microwaveprop.Radio.Contact do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
|
|
@ -33,8 +33,8 @@ defmodule Microwaveprop.Radio.Qso do
|
|||
@required_fields ~w(station1 station2 qso_timestamp mode band)a
|
||||
@optional_fields ~w(grid1 grid2 pos1 pos2 distance_km)a
|
||||
|
||||
def changeset(qso, attrs) do
|
||||
qso
|
||||
def changeset(contact, attrs) do
|
||||
contact
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
end
|
||||
|
|
@ -43,8 +43,8 @@ defmodule Microwaveprop.Radio.Qso do
|
|||
@allowed_modes ~w(CW SSB FM FT8 FT4)
|
||||
@allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 75000), &Decimal.new/1)
|
||||
|
||||
def submission_changeset(qso, attrs) do
|
||||
qso
|
||||
def submission_changeset(contact, attrs) do
|
||||
contact
|
||||
|> cast(attrs, @submission_fields)
|
||||
|> validate_required(@submission_fields)
|
||||
|> validate_grid(:grid1)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ defmodule Microwaveprop.Terrain.TerrainProfile do
|
|||
@foreign_key_type :binary_id
|
||||
|
||||
schema "terrain_profiles" do
|
||||
belongs_to :qso, Microwaveprop.Radio.Qso
|
||||
belongs_to :contact, Microwaveprop.Radio.Contact, foreign_key: :qso_id
|
||||
field :sample_count, :integer
|
||||
field :path_points, {:array, :map}
|
||||
field :max_elevation_m, :float
|
||||
|
|
|
|||
|
|
@ -126,10 +126,10 @@ defmodule Microwaveprop.Weather do
|
|||
Enum.uniq(times)
|
||||
end
|
||||
|
||||
def weather_for_qso(qso_params, opts \\ []) do
|
||||
lat = qso_params[:lat] || qso_params.lat
|
||||
lon = qso_params[:lon] || qso_params.lon
|
||||
timestamp = qso_params[:timestamp] || qso_params.timestamp
|
||||
def weather_for_contact(contact_params, opts \\ []) do
|
||||
lat = contact_params[:lat] || contact_params.lat
|
||||
lon = contact_params[:lon] || contact_params.lon
|
||||
timestamp = contact_params[:timestamp] || contact_params.timestamp
|
||||
|
||||
radius_km = Keyword.get(opts, :radius_km, 150)
|
||||
time_window_hours = Keyword.get(opts, :time_window_hours, 6)
|
||||
|
|
@ -203,14 +203,14 @@ defmodule Microwaveprop.Weather do
|
|||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def hrrr_for_qso(%{pos1: nil}), do: nil
|
||||
def hrrr_for_contact(%{pos1: nil}), do: nil
|
||||
|
||||
def hrrr_for_qso(qso) do
|
||||
lat = qso.pos1["lat"]
|
||||
lon = qso.pos1["lon"] || qso.pos1["lng"]
|
||||
def hrrr_for_contact(contact) do
|
||||
lat = contact.pos1["lat"]
|
||||
lon = contact.pos1["lon"] || contact.pos1["lng"]
|
||||
|
||||
if lat && lon do
|
||||
find_nearest_hrrr(lat, lon, qso.qso_timestamp)
|
||||
find_nearest_hrrr(lat, lon, contact.qso_timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -245,10 +245,10 @@ defmodule Microwaveprop.Weather do
|
|||
|
||||
def hrrr_profiles_for_path(%{pos1: nil}), do: []
|
||||
|
||||
def hrrr_profiles_for_path(qso) do
|
||||
qso
|
||||
|> Microwaveprop.Radio.qso_path_points()
|
||||
|> Enum.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, qso.qso_timestamp) end)
|
||||
def hrrr_profiles_for_path(contact) do
|
||||
contact
|
||||
|> Microwaveprop.Radio.contact_path_points()
|
||||
|> Enum.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, contact.qso_timestamp) end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
|
|
@ -301,14 +301,14 @@ defmodule Microwaveprop.Weather do
|
|||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def iemre_for_qso(%{pos1: nil}), do: nil
|
||||
def iemre_for_contact(%{pos1: nil}), do: nil
|
||||
|
||||
def iemre_for_qso(qso) do
|
||||
lat = qso.pos1["lat"]
|
||||
lon = qso.pos1["lon"] || qso.pos1["lng"]
|
||||
def iemre_for_contact(contact) do
|
||||
lat = contact.pos1["lat"]
|
||||
lon = contact.pos1["lon"] || contact.pos1["lng"]
|
||||
|
||||
if lat && lon do
|
||||
find_nearest_iemre(lat, lon, qso.qso_timestamp)
|
||||
find_nearest_iemre(lat, lon, contact.qso_timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -323,10 +323,10 @@ defmodule Microwaveprop.Weather do
|
|||
|
||||
def iemre_for_path(%{pos1: nil}), do: []
|
||||
|
||||
def iemre_for_path(qso) do
|
||||
qso
|
||||
|> Microwaveprop.Radio.qso_path_points()
|
||||
|> Enum.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, qso.qso_timestamp) end)
|
||||
def iemre_for_path(contact) do
|
||||
contact
|
||||
|> Microwaveprop.Radio.contact_path_points()
|
||||
|> Enum.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, contact.qso_timestamp) end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
|
||||
defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|
||||
@moduledoc false
|
||||
use Oban.Worker, queue: :enqueue, max_attempts: 3
|
||||
|
||||
|
|
@ -16,14 +16,14 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
|
|||
@insert_batch_size 400
|
||||
|
||||
@doc """
|
||||
Enqueue all enrichment jobs (weather, HRRR, terrain, IEMRE) for a single QSO.
|
||||
Enqueue all enrichment jobs (weather, HRRR, terrain, IEMRE) for a single contact.
|
||||
Called directly from submission flow — no Oban indirection.
|
||||
"""
|
||||
def enqueue_for_qso(qso) do
|
||||
weather_jobs = build_weather_jobs([qso])
|
||||
hrrr_jobs = build_hrrr_jobs([qso])
|
||||
terrain_jobs = build_terrain_jobs([qso])
|
||||
iemre_jobs = build_iemre_jobs([qso])
|
||||
def enqueue_for_contact(contact) do
|
||||
weather_jobs = build_weather_jobs([contact])
|
||||
hrrr_jobs = build_hrrr_jobs([contact])
|
||||
terrain_jobs = build_terrain_jobs([contact])
|
||||
iemre_jobs = build_iemre_jobs([contact])
|
||||
|
||||
all_jobs = weather_jobs ++ hrrr_jobs ++ terrain_jobs ++ iemre_jobs
|
||||
|
||||
|
|
@ -31,11 +31,11 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
|
|||
insert_all_chunked(all_jobs)
|
||||
end
|
||||
|
||||
qso_ids = [qso.id]
|
||||
if qso.pos1, do: Radio.mark_weather_queued!(qso_ids)
|
||||
if qso.pos1, do: Radio.mark_hrrr_queued!(qso_ids)
|
||||
if qso.pos1 && qso.pos2, do: Radio.mark_terrain_queued!(qso_ids)
|
||||
if qso.pos1, do: Radio.mark_iemre_queued!(qso_ids)
|
||||
ids = [contact.id]
|
||||
if contact.pos1, do: Radio.mark_weather_queued!(ids)
|
||||
if contact.pos1, do: Radio.mark_hrrr_queued!(ids)
|
||||
if contact.pos1 && contact.pos2, do: Radio.mark_terrain_queued!(ids)
|
||||
if contact.pos1, do: Radio.mark_iemre_queued!(ids)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
|
@ -51,85 +51,82 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
|
|||
end
|
||||
|
||||
defp enqueue_weather_jobs do
|
||||
case Radio.unprocessed_qsos() do
|
||||
case Radio.unprocessed_contacts() do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
qsos ->
|
||||
Radio.backfill_distances(qsos)
|
||||
contacts ->
|
||||
Radio.backfill_distances(contacts)
|
||||
|
||||
jobs = build_weather_jobs(qsos)
|
||||
jobs = build_weather_jobs(contacts)
|
||||
|
||||
if jobs != [] do
|
||||
insert_all_chunked(jobs)
|
||||
end
|
||||
|
||||
qso_ids = Enum.map(qsos, & &1.id)
|
||||
Radio.mark_weather_queued!(qso_ids)
|
||||
ids = Enum.map(contacts, & &1.id)
|
||||
Radio.mark_weather_queued!(ids)
|
||||
|
||||
enqueue_weather_jobs()
|
||||
end
|
||||
end
|
||||
|
||||
defp enqueue_hrrr_jobs do
|
||||
case Radio.unprocessed_hrrr_qsos() do
|
||||
case Radio.unprocessed_hrrr_contacts() do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
qsos ->
|
||||
jobs = build_hrrr_jobs(qsos)
|
||||
contacts ->
|
||||
jobs = build_hrrr_jobs(contacts)
|
||||
|
||||
if jobs != [] do
|
||||
insert_all_chunked(jobs)
|
||||
end
|
||||
|
||||
qso_ids = Enum.map(qsos, & &1.id)
|
||||
Radio.mark_hrrr_queued!(qso_ids)
|
||||
ids = Enum.map(contacts, & &1.id)
|
||||
Radio.mark_hrrr_queued!(ids)
|
||||
|
||||
enqueue_hrrr_jobs()
|
||||
end
|
||||
end
|
||||
|
||||
defp enqueue_terrain_jobs do
|
||||
case Radio.unprocessed_terrain_qsos() do
|
||||
case Radio.unprocessed_terrain_contacts() do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
qsos ->
|
||||
jobs = build_terrain_jobs(qsos)
|
||||
contacts ->
|
||||
jobs = build_terrain_jobs(contacts)
|
||||
|
||||
if jobs != [] do
|
||||
insert_all_chunked(jobs)
|
||||
end
|
||||
|
||||
qso_ids = Enum.map(qsos, & &1.id)
|
||||
Radio.mark_terrain_queued!(qso_ids)
|
||||
ids = Enum.map(contacts, & &1.id)
|
||||
Radio.mark_terrain_queued!(ids)
|
||||
|
||||
# Continue until all QSOs are enqueued
|
||||
enqueue_terrain_jobs()
|
||||
end
|
||||
end
|
||||
|
||||
def build_terrain_jobs(qsos) do
|
||||
qsos
|
||||
|> Enum.filter(fn qso -> qso.pos1 && qso.pos2 end)
|
||||
|> Enum.map(fn qso ->
|
||||
TerrainProfileWorker.new(%{"qso_id" => qso.id})
|
||||
def build_terrain_jobs(contacts) do
|
||||
contacts
|
||||
|> Enum.filter(fn contact -> contact.pos1 && contact.pos2 end)
|
||||
|> Enum.map(fn contact ->
|
||||
TerrainProfileWorker.new(%{"qso_id" => contact.id})
|
||||
end)
|
||||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||||
end
|
||||
|
||||
def build_weather_jobs(qsos) do
|
||||
qsos
|
||||
|> Enum.flat_map(&jobs_for_qso/1)
|
||||
def build_weather_jobs(contacts) do
|
||||
contacts
|
||||
|> Enum.flat_map(&jobs_for_contact/1)
|
||||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||||
end
|
||||
|
||||
def build_hrrr_jobs(qsos) do
|
||||
# Group all QSO points by HRRR hour, then create one batch job per hour
|
||||
# instead of one job per point. This deduplicates GRIB2 downloads.
|
||||
qsos
|
||||
|> Enum.flat_map(&hrrr_points_for_qso/1)
|
||||
def build_hrrr_jobs(contacts) do
|
||||
contacts
|
||||
|> Enum.flat_map(&hrrr_points_for_contact/1)
|
||||
|> Enum.group_by(fn {_point, hour} -> hour end, fn {point, _hour} -> point end)
|
||||
|> Enum.map(fn {hour, points} ->
|
||||
unique_points =
|
||||
|
|
@ -144,47 +141,47 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
|
|||
end)
|
||||
end
|
||||
|
||||
def build_iemre_jobs(qsos) do
|
||||
qsos
|
||||
|> Enum.flat_map(&iemre_job_for_qso/1)
|
||||
def build_iemre_jobs(contacts) do
|
||||
contacts
|
||||
|> Enum.flat_map(&iemre_job_for_contact/1)
|
||||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||||
end
|
||||
|
||||
defp jobs_for_qso(qso) do
|
||||
qso
|
||||
|> Radio.qso_path_points()
|
||||
defp jobs_for_contact(contact) do
|
||||
contact
|
||||
|> Radio.contact_path_points()
|
||||
|> Enum.flat_map(fn {lat, lon} ->
|
||||
build_asos_jobs(lat, lon, qso.qso_timestamp) ++
|
||||
build_raob_jobs(lat, lon, qso.qso_timestamp)
|
||||
build_asos_jobs(lat, lon, contact.qso_timestamp) ++
|
||||
build_raob_jobs(lat, lon, contact.qso_timestamp)
|
||||
end)
|
||||
end
|
||||
|
||||
defp enqueue_iemre_jobs do
|
||||
case Radio.unprocessed_iemre_qsos() do
|
||||
case Radio.unprocessed_iemre_contacts() do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
qsos ->
|
||||
jobs = build_iemre_jobs(qsos)
|
||||
contacts ->
|
||||
jobs = build_iemre_jobs(contacts)
|
||||
|
||||
if jobs != [] do
|
||||
insert_all_chunked(jobs)
|
||||
end
|
||||
|
||||
qso_ids = Enum.map(qsos, & &1.id)
|
||||
Radio.mark_iemre_queued!(qso_ids)
|
||||
ids = Enum.map(contacts, & &1.id)
|
||||
Radio.mark_iemre_queued!(ids)
|
||||
|
||||
enqueue_iemre_jobs()
|
||||
end
|
||||
end
|
||||
|
||||
defp hrrr_points_for_qso(%{pos1: nil}), do: []
|
||||
defp hrrr_points_for_contact(%{pos1: nil}), do: []
|
||||
|
||||
defp hrrr_points_for_qso(qso) do
|
||||
rounded_time = HrrrClient.nearest_hrrr_hour(qso.qso_timestamp)
|
||||
defp hrrr_points_for_contact(contact) do
|
||||
rounded_time = HrrrClient.nearest_hrrr_hour(contact.qso_timestamp)
|
||||
|
||||
qso
|
||||
|> Radio.qso_path_points()
|
||||
contact
|
||||
|> Radio.contact_path_points()
|
||||
|> Enum.flat_map(fn {lat, lon} ->
|
||||
{rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon)
|
||||
|
||||
|
|
@ -216,13 +213,13 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
|
|||
end)
|
||||
end
|
||||
|
||||
defp iemre_job_for_qso(%{pos1: nil}), do: []
|
||||
defp iemre_job_for_contact(%{pos1: nil}), do: []
|
||||
|
||||
defp iemre_job_for_qso(qso) do
|
||||
date = DateTime.to_date(qso.qso_timestamp)
|
||||
defp iemre_job_for_contact(contact) do
|
||||
date = DateTime.to_date(contact.qso_timestamp)
|
||||
|
||||
qso
|
||||
|> Radio.qso_path_points()
|
||||
contact
|
||||
|> Radio.contact_path_points()
|
||||
|> Enum.flat_map(fn {lat, lon} ->
|
||||
{rlat, rlon} = Weather.round_to_iemre_grid(lat, lon)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,16 +18,16 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
|||
if Terrain.has_terrain_profile?(qso_id) do
|
||||
:ok
|
||||
else
|
||||
qso = Radio.get_qso!(qso_id)
|
||||
lat1 = qso.pos1["lat"]
|
||||
lon1 = qso.pos1["lon"] || qso.pos1["lng"]
|
||||
lat2 = qso.pos2["lat"]
|
||||
lon2 = qso.pos2["lon"] || qso.pos2["lng"]
|
||||
dist_km = Decimal.to_float(qso.distance_km)
|
||||
freq_ghz = Decimal.to_float(qso.band) / 1000
|
||||
contact = Radio.get_contact!(qso_id)
|
||||
lat1 = contact.pos1["lat"]
|
||||
lon1 = contact.pos1["lon"] || contact.pos1["lng"]
|
||||
lat2 = contact.pos2["lat"]
|
||||
lon2 = contact.pos2["lon"] || contact.pos2["lng"]
|
||||
dist_km = Decimal.to_float(contact.distance_km)
|
||||
freq_ghz = Decimal.to_float(contact.band) / 1000
|
||||
|
||||
# Look up HRRR refractivity gradient for dynamic k-factor
|
||||
k = lookup_k_factor(qso)
|
||||
k = lookup_k_factor(contact)
|
||||
|
||||
case ElevationClient.fetch_elevation_profile(lat1, lon1, lat2, lon2) do
|
||||
{:ok, profile} ->
|
||||
|
|
@ -64,8 +64,8 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
|||
end
|
||||
end
|
||||
|
||||
defp lookup_k_factor(qso) do
|
||||
case Weather.hrrr_for_qso(qso) do
|
||||
defp lookup_k_factor(contact) do
|
||||
case Weather.hrrr_for_contact(contact) do
|
||||
%{min_refractivity_gradient: grad} when not is_nil(grad) ->
|
||||
TerrainAnalysis.k_factor(grad)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,4 +4,12 @@ defmodule MicrowavepropWeb.PageController do
|
|||
def home(conn, _params) do
|
||||
redirect(conn, to: ~p"/map")
|
||||
end
|
||||
|
||||
def redirect_contacts(conn, _params) do
|
||||
redirect(conn, to: ~p"/contacts")
|
||||
end
|
||||
|
||||
def redirect_contact(conn, %{"id" => id}) do
|
||||
redirect(conn, to: ~p"/contacts/#{id}")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule MicrowavepropWeb.QsoLive.Index do
|
||||
defmodule MicrowavepropWeb.ContactLive.Index do
|
||||
@moduledoc false
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
search = Map.get(params, "search", "")
|
||||
|
||||
result =
|
||||
Radio.list_qsos(
|
||||
Radio.list_contacts(
|
||||
page: page,
|
||||
sort_by: String.to_existing_atom(sort_by),
|
||||
sort_order: String.to_existing_atom(sort_order),
|
||||
|
|
@ -44,7 +44,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => search}, socket) do
|
||||
{:noreply, push_patch(socket, to: ~p"/qsos?search=#{search}")}
|
||||
{:noreply, push_patch(socket, to: ~p"/contacts?search=#{search}")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -58,7 +58,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
|
||||
params = %{sort_by: field, sort_order: new_order}
|
||||
params = if socket.assigns.search == "", do: params, else: Map.put(params, :search, socket.assigns.search)
|
||||
{:noreply, push_patch(socket, to: ~p"/qsos?#{params}")}
|
||||
{:noreply, push_patch(socket, to: ~p"/contacts?#{params}")}
|
||||
end
|
||||
|
||||
defp validate_sort_field(field) when field in @sortable_fields, do: field
|
||||
|
|
@ -91,7 +91,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
class="input input-bordered input-sm w-64"
|
||||
/>
|
||||
<button type="submit" class="btn btn-sm btn-outline">Search</button>
|
||||
<.link :if={@search != ""} patch={~p"/qsos"} class="btn btn-sm btn-ghost">Clear</.link>
|
||||
<.link :if={@search != ""} patch={~p"/contacts"} class="btn btn-sm btn-ghost">Clear</.link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
"hover:bg-base-200 cursor-pointer",
|
||||
reciprocals != [] && "bg-primary/5"
|
||||
]}
|
||||
phx-click={JS.navigate(~p"/qsos/#{primary.id}")}
|
||||
phx-click={JS.navigate(~p"/contacts/#{primary.id}")}
|
||||
>
|
||||
<td class="w-8 text-center">
|
||||
<span
|
||||
|
|
@ -144,7 +144,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
<%= for recip <- reciprocals do %>
|
||||
<tr
|
||||
class="hover:bg-base-200 cursor-pointer bg-primary/5 opacity-70 border-t-0"
|
||||
phx-click={JS.navigate(~p"/qsos/#{recip.id}")}
|
||||
phx-click={JS.navigate(~p"/contacts/#{recip.id}")}
|
||||
>
|
||||
<td class="w-8 text-center text-primary">↳</td>
|
||||
<td class="font-mono">{recip.station1}</td>
|
||||
|
|
@ -166,7 +166,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
<.link
|
||||
:if={@page > 1}
|
||||
patch={
|
||||
~p"/qsos?page=#{@page - 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
|
||||
~p"/contacts?page=#{@page - 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
|
||||
}
|
||||
class="btn btn-sm btn-outline"
|
||||
>
|
||||
|
|
@ -179,7 +179,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
|
|||
<.link
|
||||
:if={@page < @total_pages}
|
||||
patch={
|
||||
~p"/qsos?page=#{@page + 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
|
||||
~p"/contacts?page=#{@page + 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
|
||||
}
|
||||
class="btn btn-sm btn-outline"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule MicrowavepropWeb.QsoLive.Show do
|
||||
defmodule MicrowavepropWeb.ContactLive.Show do
|
||||
@moduledoc false
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
|
|
@ -8,17 +8,17 @@ defmodule MicrowavepropWeb.QsoLive.Show do
|
|||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
qso = Radio.get_qso!(id)
|
||||
contact = Radio.get_contact!(id)
|
||||
|
||||
weather = load_weather(qso)
|
||||
solar = load_solar(qso)
|
||||
hrrr = Weather.hrrr_for_qso(qso)
|
||||
terrain = Terrain.get_terrain_profile(qso.id)
|
||||
weather = load_weather(contact)
|
||||
solar = load_solar(contact)
|
||||
hrrr = Weather.hrrr_for_contact(contact)
|
||||
terrain = Terrain.get_terrain_profile(contact.id)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
page_title: "#{qso.station1} / #{qso.station2}",
|
||||
qso: qso,
|
||||
page_title: "#{contact.station1} / #{contact.station2}",
|
||||
contact: contact,
|
||||
surface_observations: weather.surface_observations,
|
||||
soundings: weather.soundings,
|
||||
solar: solar,
|
||||
|
|
@ -117,12 +117,12 @@ defmodule MicrowavepropWeb.QsoLive.Show do
|
|||
defp sounding_sort_key("lifted_index"), do: &(&1.lifted_index || 0)
|
||||
defp sounding_sort_key(_), do: fn s -> s.station.name || s.station.station_code end
|
||||
|
||||
defp load_weather(qso) do
|
||||
lat = qso.pos1 && qso.pos1["lat"]
|
||||
lon = qso.pos1 && (qso.pos1["lon"] || qso.pos1["lng"])
|
||||
defp load_weather(contact) do
|
||||
lat = contact.pos1 && contact.pos1["lat"]
|
||||
lon = contact.pos1 && (contact.pos1["lon"] || contact.pos1["lng"])
|
||||
|
||||
if lat && lon do
|
||||
Weather.weather_for_qso(%{lat: lat, lon: lon, timestamp: qso.qso_timestamp},
|
||||
Weather.weather_for_contact(%{lat: lat, lon: lon, timestamp: contact.qso_timestamp},
|
||||
radius_km: 300
|
||||
)
|
||||
else
|
||||
|
|
@ -130,8 +130,8 @@ defmodule MicrowavepropWeb.QsoLive.Show do
|
|||
end
|
||||
end
|
||||
|
||||
defp load_solar(qso) do
|
||||
qso.qso_timestamp
|
||||
defp load_solar(contact) do
|
||||
contact.qso_timestamp
|
||||
|> DateTime.to_date()
|
||||
|> Weather.get_solar_index()
|
||||
end
|
||||
|
|
@ -141,10 +141,10 @@ defmodule MicrowavepropWeb.QsoLive.Show do
|
|||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{@qso.station1} / {@qso.station2}
|
||||
<:subtitle>{Calendar.strftime(@qso.qso_timestamp, "%Y-%m-%d %H:%M UTC")}</:subtitle>
|
||||
{@contact.station1} / {@contact.station2}
|
||||
<:subtitle>{Calendar.strftime(@contact.qso_timestamp, "%Y-%m-%d %H:%M UTC")}</:subtitle>
|
||||
<:actions>
|
||||
<.link navigate={~p"/qsos"} class="btn btn-sm btn-ghost">
|
||||
<.link navigate={~p"/contacts"} class="btn btn-sm btn-ghost">
|
||||
<.icon name="hero-arrow-left" class="w-4 h-4" /> Back to QSOs
|
||||
</.link>
|
||||
</:actions>
|
||||
|
|
@ -154,19 +154,21 @@ defmodule MicrowavepropWeb.QsoLive.Show do
|
|||
|
||||
<h2 class="text-base font-semibold mb-2">QSO Details</h2>
|
||||
<.list>
|
||||
<:item title="Station 1">{@qso.station1}</:item>
|
||||
<:item title="Station 2">{@qso.station2}</:item>
|
||||
<:item title="Band">{@qso.band} MHz</:item>
|
||||
<:item title="Mode">{@qso.mode}</:item>
|
||||
<:item title="Grid 1">{@qso.grid1 || "—"}</:item>
|
||||
<:item title="Grid 2">{@qso.grid2 || "—"}</:item>
|
||||
<:item title="Distance">{@qso.distance_km || "—"} km</:item>
|
||||
<:item title="Timestamp">{Calendar.strftime(@qso.qso_timestamp, "%Y-%m-%d %H:%M UTC")}</:item>
|
||||
<:item :if={@qso.user_submitted} title="Source">
|
||||
<:item title="Station 1">{@contact.station1}</:item>
|
||||
<:item title="Station 2">{@contact.station2}</:item>
|
||||
<:item title="Band">{@contact.band} MHz</:item>
|
||||
<:item title="Mode">{@contact.mode}</:item>
|
||||
<:item title="Grid 1">{@contact.grid1 || "—"}</:item>
|
||||
<:item title="Grid 2">{@contact.grid2 || "—"}</:item>
|
||||
<:item title="Distance">{@contact.distance_km || "—"} km</:item>
|
||||
<:item title="Timestamp">
|
||||
{Calendar.strftime(@contact.qso_timestamp, "%Y-%m-%d %H:%M UTC")}
|
||||
</:item>
|
||||
<:item :if={@contact.user_submitted} title="Source">
|
||||
<span class="badge badge-info badge-sm">User Submitted</span>
|
||||
</:item>
|
||||
<:item :if={@qso.user_submitted && @qso.submitter_email} title="Submitter">
|
||||
{@qso.submitter_email}
|
||||
<:item :if={@contact.user_submitted && @contact.submitter_email} title="Submitter">
|
||||
{@contact.submitter_email}
|
||||
</:item>
|
||||
</.list>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Radio
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Workers.QsoWeatherEnqueueWorker
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Workers.ContactWeatherEnqueueWorker
|
||||
|
||||
@band_options [
|
||||
{"1296 MHz", "1296"},
|
||||
|
|
@ -21,11 +21,11 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
changeset = Radio.change_qso(%Qso{})
|
||||
changeset = Radio.change_contact(%Contact{})
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
page_title: "Submit QSO",
|
||||
page_title: "Submit Contact",
|
||||
form: to_form(changeset),
|
||||
band_options: @band_options,
|
||||
mode_options: @mode_options,
|
||||
|
|
@ -34,10 +34,10 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"qso" => qso_params}, socket) do
|
||||
def handle_event("validate", %{"contact" => contact_params}, socket) do
|
||||
changeset =
|
||||
%Qso{}
|
||||
|> Radio.change_qso(qso_params)
|
||||
%Contact{}
|
||||
|> Radio.change_contact(contact_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
|
|
@ -47,19 +47,19 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
# For broader IP-based rate limiting, use a reverse proxy (nginx limit_req, Cloudflare).
|
||||
@submission_cooldown_ms 30_000
|
||||
|
||||
def handle_event("save", %{"qso" => qso_params}, socket) do
|
||||
def handle_event("save", %{"contact" => contact_params}, socket) do
|
||||
if recently_submitted?(socket) do
|
||||
{:noreply, put_flash(socket, :error, "Please wait before submitting another QSO.")}
|
||||
{:noreply, put_flash(socket, :error, "Please wait before submitting another contact.")}
|
||||
else
|
||||
case Radio.create_qso(qso_params) do
|
||||
{:ok, qso} ->
|
||||
QsoWeatherEnqueueWorker.enqueue_for_qso(qso)
|
||||
case Radio.create_contact(contact_params) do
|
||||
{:ok, contact} ->
|
||||
ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(submitted_at: System.monotonic_time(:millisecond))
|
||||
|> put_flash(:info, "QSO submitted successfully!")
|
||||
|> push_navigate(to: ~p"/qsos/#{qso.id}")}
|
||||
|> put_flash(:info, "Contact submitted successfully!")
|
||||
|> push_navigate(to: ~p"/contacts/#{contact.id}")}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
|
|
@ -79,11 +79,11 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Submit QSO
|
||||
<:subtitle>Submit a microwave QSO for propagation analysis</:subtitle>
|
||||
Submit Contact
|
||||
<:subtitle>Submit a microwave contact for propagation analysis</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="qso-form" phx-change="validate" phx-submit="save" class="space-y-4">
|
||||
<.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">
|
||||
<.input field={@form[:station1]} type="text" label="Station 1" placeholder="W5XD" />
|
||||
<.input
|
||||
|
|
@ -133,7 +133,7 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
/>
|
||||
|
||||
<div class="mt-6">
|
||||
<.button phx-disable-with="Submitting..." class="btn-primary">Submit QSO</.button>
|
||||
<.button phx-disable-with="Submitting..." class="btn-primary">Submit Contact</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,13 @@ defmodule MicrowavepropWeb.Router do
|
|||
|
||||
live "/submit", SubmitLive
|
||||
live "/map", MapLive
|
||||
live "/qsos", QsoLive.Index
|
||||
live "/qsos/:id", QsoLive.Show
|
||||
live "/contacts", ContactLive.Index
|
||||
live "/contacts/:id", ContactLive.Show
|
||||
live "/algo", AlgoLive
|
||||
|
||||
# Redirect old /qsos routes
|
||||
get "/qsos", PageController, :redirect_contacts
|
||||
get "/qsos/:id", PageController, :redirect_contact
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ defmodule Mix.Tasks.PropagationTrain do
|
|||
|
||||
IO.puts("=" <> String.duplicate("─", 50))
|
||||
|
||||
{ft_features, ft_targets, ft_counts} = load_qso_data()
|
||||
{ft_features, ft_targets, ft_counts} = load_contact_data()
|
||||
ft_n = elem(Nx.shape(ft_features), 0)
|
||||
IO.puts("Loaded #{ft_n} QSO training samples:")
|
||||
print_band_counts(ft_counts)
|
||||
|
|
@ -247,7 +247,7 @@ defmodule Mix.Tasks.PropagationTrain do
|
|||
|
||||
# ── Phase 2 data: QSO-HRRR + solar + sounding ────────────────────
|
||||
|
||||
defp load_qso_data do
|
||||
defp load_contact_data do
|
||||
IO.puts(" Running QSO-HRRR-solar-sounding join query...")
|
||||
|
||||
sql = """
|
||||
|
|
@ -326,7 +326,7 @@ defmodule Mix.Tasks.PropagationTrain do
|
|||
|
||||
{feature_rows, target_rows} =
|
||||
percentile_rows
|
||||
|> Enum.map(fn {row, percentile} -> encode_qso_row(row, percentile) end)
|
||||
|> Enum.map(fn {row, percentile} -> encode_contact_row(row, percentile) end)
|
||||
|> Enum.unzip()
|
||||
|
||||
IO.puts(" After filtering: #{length(feature_rows)} training samples")
|
||||
|
|
@ -334,7 +334,7 @@ defmodule Mix.Tasks.PropagationTrain do
|
|||
{Nx.tensor(feature_rows, type: :f32), Nx.tensor(target_rows, type: :f32), band_counts}
|
||||
end
|
||||
|
||||
defp encode_qso_row(
|
||||
defp encode_contact_row(
|
||||
[
|
||||
band_mhz,
|
||||
_dist,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Mix.Tasks.ResetEnrichment do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
@impl Mix.Task
|
||||
|
|
@ -23,7 +23,7 @@ defmodule Mix.Tasks.ResetEnrichment do
|
|||
Mix.Task.run("app.start")
|
||||
|
||||
{count, _} =
|
||||
Qso
|
||||
Contact
|
||||
|> where([q], q.weather_queued == true or q.hrrr_queued == true or q.iemre_queued == true)
|
||||
|> Repo.update_all(set: [weather_queued: false, hrrr_queued: false, iemre_queued: false])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Microwaveprop.Radio.QsoSubmissionTest do
|
||||
defmodule Microwaveprop.Radio.ContactSubmissionTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
|
||||
@valid_attrs %{
|
||||
station1: "W5XD",
|
||||
|
|
@ -16,12 +16,12 @@ defmodule Microwaveprop.Radio.QsoSubmissionTest do
|
|||
|
||||
describe "submission_changeset/2" do
|
||||
test "valid attrs produce a valid changeset" do
|
||||
changeset = Qso.submission_changeset(%Qso{}, @valid_attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, @valid_attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "requires station1, station2, qso_timestamp, mode, band, grid1, grid2, submitter_email" do
|
||||
changeset = Qso.submission_changeset(%Qso{}, %{})
|
||||
changeset = Contact.submission_changeset(%Contact{}, %{})
|
||||
errors = errors_on(changeset)
|
||||
|
||||
assert errors[:station1]
|
||||
|
|
@ -36,59 +36,59 @@ defmodule Microwaveprop.Radio.QsoSubmissionTest do
|
|||
|
||||
test "rejects invalid grid1" do
|
||||
attrs = Map.put(@valid_attrs, :grid1, "ZZ99")
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid1
|
||||
end
|
||||
|
||||
test "rejects invalid grid2" do
|
||||
attrs = Map.put(@valid_attrs, :grid2, "ZZ99")
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid2
|
||||
end
|
||||
|
||||
test "accepts valid 6-char grids" do
|
||||
attrs = %{@valid_attrs | grid1: "EM12ab", grid2: "EM00cd"}
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "rejects invalid mode" do
|
||||
attrs = Map.put(@valid_attrs, :mode, "RTTY")
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert errors_on(changeset).mode
|
||||
end
|
||||
|
||||
test "accepts valid modes" do
|
||||
for mode <- ~w(CW SSB FM FT8 FT4) do
|
||||
attrs = Map.put(@valid_attrs, :mode, mode)
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert changeset.valid?, "Expected mode #{mode} to be valid"
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects invalid band" do
|
||||
attrs = Map.put(@valid_attrs, :band, "144")
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert errors_on(changeset).band
|
||||
end
|
||||
|
||||
test "accepts valid microwave bands" do
|
||||
for band <- ~w(1296 2304 3456 5760 10000 24000 47000 75000) do
|
||||
attrs = Map.put(@valid_attrs, :band, band)
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert changeset.valid?, "Expected band #{band} to be valid"
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects email without @" do
|
||||
attrs = Map.put(@valid_attrs, :submitter_email, "notanemail")
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
assert errors_on(changeset).submitter_email
|
||||
end
|
||||
|
||||
test "does not cast user_submitted" do
|
||||
attrs = Map.put(@valid_attrs, :user_submitted, true)
|
||||
changeset = Qso.submission_changeset(%Qso{}, attrs)
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
refute Ecto.Changeset.get_change(changeset, :user_submitted)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Microwaveprop.Radio.QsoTest do
|
||||
defmodule Microwaveprop.Radio.ContactTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
|
||||
@valid_attrs %{
|
||||
station1: "W1AW",
|
||||
|
|
@ -18,12 +18,12 @@ defmodule Microwaveprop.Radio.QsoTest do
|
|||
|
||||
describe "changeset/2" do
|
||||
test "valid attributes" do
|
||||
changeset = Qso.changeset(%Qso{}, @valid_attrs)
|
||||
changeset = Contact.changeset(%Contact{}, @valid_attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "requires station1, station2, qso_timestamp, mode, and band" do
|
||||
changeset = Qso.changeset(%Qso{}, %{})
|
||||
changeset = Contact.changeset(%Contact{}, %{})
|
||||
|
||||
assert %{
|
||||
station1: ["can't be blank"],
|
||||
|
|
@ -35,13 +35,13 @@ defmodule Microwaveprop.Radio.QsoTest do
|
|||
end
|
||||
|
||||
test "persists to the database" do
|
||||
changeset = Qso.changeset(%Qso{}, @valid_attrs)
|
||||
assert {:ok, qso} = Repo.insert(changeset)
|
||||
assert qso.station1 == "W1AW"
|
||||
assert qso.station2 == "K3LR"
|
||||
assert qso.mode == "SSB"
|
||||
assert Decimal.equal?(qso.band, Decimal.new("144.2"))
|
||||
assert Decimal.equal?(qso.distance_km, Decimal.new("623.4"))
|
||||
changeset = Contact.changeset(%Contact{}, @valid_attrs)
|
||||
assert {:ok, contact} = Repo.insert(changeset)
|
||||
assert contact.station1 == "W1AW"
|
||||
assert contact.station2 == "K3LR"
|
||||
assert contact.mode == "SSB"
|
||||
assert Decimal.equal?(contact.band, Decimal.new("144.2"))
|
||||
assert Decimal.equal?(contact.distance_km, Decimal.new("623.4"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ defmodule Microwaveprop.RadioTest do
|
|||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
|
||||
defp create_qso(attrs \\ %{}) do
|
||||
defp create_contact(attrs \\ %{}) do
|
||||
default = %{
|
||||
station1: "W5XD",
|
||||
station2: "K5TR",
|
||||
|
|
@ -18,17 +18,17 @@ defmodule Microwaveprop.RadioTest do
|
|||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
{:ok, qso} =
|
||||
%Qso{}
|
||||
|> Qso.changeset(Map.merge(default, attrs))
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(Map.merge(default, attrs))
|
||||
|> Repo.insert()
|
||||
|
||||
qso
|
||||
contact
|
||||
end
|
||||
|
||||
describe "list_qsos/1" do
|
||||
describe "list_contacts/1" do
|
||||
test "returns empty page when no QSOs exist" do
|
||||
result = Radio.list_qsos()
|
||||
result = Radio.list_contacts()
|
||||
|
||||
assert result.entries == []
|
||||
assert result.page == 1
|
||||
|
|
@ -39,10 +39,10 @@ defmodule Microwaveprop.RadioTest do
|
|||
test "paginates 20 per page, ordered by qso_timestamp desc" do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
create_qso(%{qso_timestamp: ts})
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
result = Radio.list_qsos()
|
||||
result = Radio.list_contacts()
|
||||
|
||||
assert length(result.entries) == 20
|
||||
assert result.page == 1
|
||||
|
|
@ -57,10 +57,10 @@ defmodule Microwaveprop.RadioTest do
|
|||
test "returns second page" do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
create_qso(%{qso_timestamp: ts})
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
result = Radio.list_qsos(page: 2)
|
||||
result = Radio.list_contacts(page: 2)
|
||||
|
||||
assert length(result.entries) == 5
|
||||
assert result.page == 2
|
||||
|
|
@ -68,60 +68,60 @@ defmodule Microwaveprop.RadioTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "list_qsos/1 sorting" do
|
||||
describe "list_contacts/1 sorting" do
|
||||
test "sorts by station1 ascending" do
|
||||
create_qso(%{station1: "ZZ9ZZ"})
|
||||
create_qso(%{station1: "AA1AA"})
|
||||
create_contact(%{station1: "ZZ9ZZ"})
|
||||
create_contact(%{station1: "AA1AA"})
|
||||
|
||||
result = Radio.list_qsos(sort_by: :station1, sort_order: :asc)
|
||||
result = Radio.list_contacts(sort_by: :station1, sort_order: :asc)
|
||||
|
||||
stations = Enum.map(result.entries, & &1.station1)
|
||||
assert stations == ["AA1AA", "ZZ9ZZ"]
|
||||
end
|
||||
|
||||
test "sorts by distance_km descending" do
|
||||
create_qso(%{distance_km: Decimal.new("100"), station1: "A1A"})
|
||||
create_qso(%{distance_km: Decimal.new("500"), station1: "B2B"})
|
||||
create_contact(%{distance_km: Decimal.new("100"), station1: "A1A"})
|
||||
create_contact(%{distance_km: Decimal.new("500"), station1: "B2B"})
|
||||
|
||||
result = Radio.list_qsos(sort_by: :distance_km, sort_order: :desc)
|
||||
result = Radio.list_contacts(sort_by: :distance_km, sort_order: :desc)
|
||||
|
||||
distances = Enum.map(result.entries, & &1.distance_km)
|
||||
assert distances == [Decimal.new("500"), Decimal.new("100")]
|
||||
end
|
||||
|
||||
test "invalid sort_by falls back to qso_timestamp desc" do
|
||||
early = create_qso(%{qso_timestamp: ~U[2026-01-01 00:00:00Z]})
|
||||
late = create_qso(%{qso_timestamp: ~U[2026-03-01 00:00:00Z]})
|
||||
early = create_contact(%{qso_timestamp: ~U[2026-01-01 00:00:00Z]})
|
||||
late = create_contact(%{qso_timestamp: ~U[2026-03-01 00:00:00Z]})
|
||||
|
||||
result = Radio.list_qsos(sort_by: :bogus)
|
||||
result = Radio.list_contacts(sort_by: :bogus)
|
||||
|
||||
ids = Enum.map(result.entries, & &1.id)
|
||||
assert ids == [late.id, early.id]
|
||||
end
|
||||
end
|
||||
|
||||
describe "unprocessed_qsos/1" do
|
||||
describe "unprocessed_contacts/1" do
|
||||
test "returns QSOs where weather_queued is false and pos1 is not nil" do
|
||||
q1 = create_qso(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
|
||||
_q2 = create_qso(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
|
||||
q1 = create_contact(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
|
||||
_q2 = create_contact(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
|
||||
|
||||
results = Radio.unprocessed_qsos()
|
||||
results = Radio.unprocessed_contacts()
|
||||
ids = Enum.map(results, & &1.id)
|
||||
assert q1.id in ids
|
||||
end
|
||||
|
||||
test "excludes QSOs already marked as weather_queued" do
|
||||
q = create_qso()
|
||||
q = create_contact()
|
||||
Radio.mark_weather_queued!([q.id])
|
||||
|
||||
assert Radio.unprocessed_qsos() == []
|
||||
assert Radio.unprocessed_contacts() == []
|
||||
end
|
||||
|
||||
test "orders by qso_timestamp ascending" do
|
||||
q_late = create_qso(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
|
||||
q_early = create_qso(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
|
||||
q_late = create_contact(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
|
||||
q_early = create_contact(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
|
||||
|
||||
results = Radio.unprocessed_qsos()
|
||||
results = Radio.unprocessed_contacts()
|
||||
ids = Enum.map(results, & &1.id)
|
||||
assert ids == [q_early.id, q_late.id]
|
||||
end
|
||||
|
|
@ -129,32 +129,32 @@ defmodule Microwaveprop.RadioTest do
|
|||
test "respects limit parameter" do
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
create_qso(%{qso_timestamp: ts})
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
assert length(Radio.unprocessed_qsos(3)) == 3
|
||||
assert length(Radio.unprocessed_contacts(3)) == 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "mark_weather_queued!/1" do
|
||||
test "sets weather_queued to true for given IDs" do
|
||||
q1 = create_qso(%{station1: "A1A"})
|
||||
q2 = create_qso(%{station1: "B2B"})
|
||||
q1 = create_contact(%{station1: "A1A"})
|
||||
q2 = create_contact(%{station1: "B2B"})
|
||||
|
||||
Radio.mark_weather_queued!([q1.id, q2.id])
|
||||
|
||||
assert Repo.get!(Qso, q1.id).weather_queued == true
|
||||
assert Repo.get!(Qso, q2.id).weather_queued == true
|
||||
assert Repo.get!(Contact, q1.id).weather_queued == true
|
||||
assert Repo.get!(Contact, q2.id).weather_queued == true
|
||||
end
|
||||
|
||||
test "does not affect other QSOs" do
|
||||
q1 = create_qso(%{station1: "A1A"})
|
||||
q2 = create_qso(%{station1: "B2B"})
|
||||
q1 = create_contact(%{station1: "A1A"})
|
||||
q2 = create_contact(%{station1: "B2B"})
|
||||
|
||||
Radio.mark_weather_queued!([q1.id])
|
||||
|
||||
assert Repo.get!(Qso, q1.id).weather_queued == true
|
||||
assert Repo.get!(Qso, q2.id).weather_queued == false
|
||||
assert Repo.get!(Contact, q1.id).weather_queued == true
|
||||
assert Repo.get!(Contact, q2.id).weather_queued == false
|
||||
end
|
||||
|
||||
test "handles empty list" do
|
||||
|
|
@ -176,72 +176,72 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
describe "backfill_distances/1" do
|
||||
test "calculates and stores distance for QSOs missing distance_km" do
|
||||
qso = create_qso(%{distance_km: nil})
|
||||
assert is_nil(qso.distance_km)
|
||||
contact = create_contact(%{distance_km: nil})
|
||||
assert is_nil(contact.distance_km)
|
||||
|
||||
Radio.backfill_distances([qso])
|
||||
Radio.backfill_distances([contact])
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert_in_delta Decimal.to_float(updated.distance_km), 295, 5
|
||||
end
|
||||
|
||||
test "skips QSOs that already have a distance" do
|
||||
qso = create_qso(%{distance_km: Decimal.new("100")})
|
||||
contact = create_contact(%{distance_km: Decimal.new("100")})
|
||||
|
||||
Radio.backfill_distances([qso])
|
||||
Radio.backfill_distances([contact])
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.distance_km == Decimal.new("100")
|
||||
end
|
||||
|
||||
test "skips QSOs missing pos2" do
|
||||
qso = create_qso(%{distance_km: nil, pos2: nil})
|
||||
contact = create_contact(%{distance_km: nil, pos2: nil})
|
||||
|
||||
Radio.backfill_distances([qso])
|
||||
Radio.backfill_distances([contact])
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert is_nil(updated.distance_km)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_qso!/1" do
|
||||
describe "get_contact!/1" do
|
||||
test "returns a QSO by ID" do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
found = Radio.get_qso!(qso.id)
|
||||
assert found.id == qso.id
|
||||
found = Radio.get_contact!(contact.id)
|
||||
assert found.id == contact.id
|
||||
assert found.station1 == "W5XD"
|
||||
end
|
||||
|
||||
test "raises Ecto.NoResultsError on bad ID" do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Radio.get_qso!(Ecto.UUID.generate())
|
||||
Radio.get_contact!(Ecto.UUID.generate())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "unprocessed_hrrr_qsos/1" do
|
||||
describe "unprocessed_hrrr_contacts/1" do
|
||||
test "returns QSOs where hrrr_queued is false and pos1 is not nil" do
|
||||
q1 = create_qso(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
|
||||
_q2 = create_qso(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
|
||||
q1 = create_contact(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
|
||||
_q2 = create_contact(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
|
||||
|
||||
results = Radio.unprocessed_hrrr_qsos()
|
||||
results = Radio.unprocessed_hrrr_contacts()
|
||||
ids = Enum.map(results, & &1.id)
|
||||
assert q1.id in ids
|
||||
end
|
||||
|
||||
test "excludes QSOs already marked as hrrr_queued" do
|
||||
q = create_qso()
|
||||
q = create_contact()
|
||||
Radio.mark_hrrr_queued!([q.id])
|
||||
|
||||
assert Radio.unprocessed_hrrr_qsos() == []
|
||||
assert Radio.unprocessed_hrrr_contacts() == []
|
||||
end
|
||||
|
||||
test "orders by qso_timestamp ascending" do
|
||||
q_late = create_qso(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
|
||||
q_early = create_qso(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
|
||||
q_late = create_contact(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
|
||||
q_early = create_contact(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
|
||||
|
||||
results = Radio.unprocessed_hrrr_qsos()
|
||||
results = Radio.unprocessed_hrrr_contacts()
|
||||
ids = Enum.map(results, & &1.id)
|
||||
assert ids == [q_early.id, q_late.id]
|
||||
end
|
||||
|
|
@ -249,32 +249,32 @@ defmodule Microwaveprop.RadioTest do
|
|||
test "respects limit parameter" do
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
create_qso(%{qso_timestamp: ts})
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
assert length(Radio.unprocessed_hrrr_qsos(3)) == 3
|
||||
assert length(Radio.unprocessed_hrrr_contacts(3)) == 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "mark_hrrr_queued!/1" do
|
||||
test "sets hrrr_queued to true for given IDs" do
|
||||
q1 = create_qso(%{station1: "A1A"})
|
||||
q2 = create_qso(%{station1: "B2B"})
|
||||
q1 = create_contact(%{station1: "A1A"})
|
||||
q2 = create_contact(%{station1: "B2B"})
|
||||
|
||||
Radio.mark_hrrr_queued!([q1.id, q2.id])
|
||||
|
||||
assert Repo.get!(Qso, q1.id).hrrr_queued == true
|
||||
assert Repo.get!(Qso, q2.id).hrrr_queued == true
|
||||
assert Repo.get!(Contact, q1.id).hrrr_queued == true
|
||||
assert Repo.get!(Contact, q2.id).hrrr_queued == true
|
||||
end
|
||||
|
||||
test "does not affect other QSOs" do
|
||||
q1 = create_qso(%{station1: "A1A"})
|
||||
q2 = create_qso(%{station1: "B2B"})
|
||||
q1 = create_contact(%{station1: "A1A"})
|
||||
q2 = create_contact(%{station1: "B2B"})
|
||||
|
||||
Radio.mark_hrrr_queued!([q1.id])
|
||||
|
||||
assert Repo.get!(Qso, q1.id).hrrr_queued == true
|
||||
assert Repo.get!(Qso, q2.id).hrrr_queued == false
|
||||
assert Repo.get!(Contact, q1.id).hrrr_queued == true
|
||||
assert Repo.get!(Contact, q2.id).hrrr_queued == false
|
||||
end
|
||||
|
||||
test "handles empty list" do
|
||||
|
|
@ -282,30 +282,30 @@ defmodule Microwaveprop.RadioTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "unprocessed_terrain_qsos/1" do
|
||||
describe "unprocessed_terrain_contacts/1" do
|
||||
test "returns QSOs where terrain_queued is false and both pos1 and pos2 are not nil" do
|
||||
q1 = create_qso(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
|
||||
_q2 = create_qso(%{station1: "NOPOS1", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
|
||||
_q3 = create_qso(%{station1: "NOPOS2", pos2: nil, qso_timestamp: ~U[2026-03-28 14:00:00Z]})
|
||||
q1 = create_contact(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
|
||||
_q2 = create_contact(%{station1: "NOPOS1", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
|
||||
_q3 = create_contact(%{station1: "NOPOS2", pos2: nil, qso_timestamp: ~U[2026-03-28 14:00:00Z]})
|
||||
|
||||
results = Radio.unprocessed_terrain_qsos()
|
||||
results = Radio.unprocessed_terrain_contacts()
|
||||
ids = Enum.map(results, & &1.id)
|
||||
assert q1.id in ids
|
||||
assert length(ids) == 1
|
||||
end
|
||||
|
||||
test "excludes QSOs already marked as terrain_queued" do
|
||||
q = create_qso()
|
||||
q = create_contact()
|
||||
Radio.mark_terrain_queued!([q.id])
|
||||
|
||||
assert Radio.unprocessed_terrain_qsos() == []
|
||||
assert Radio.unprocessed_terrain_contacts() == []
|
||||
end
|
||||
|
||||
test "orders by qso_timestamp ascending" do
|
||||
q_late = create_qso(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
|
||||
q_early = create_qso(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
|
||||
q_late = create_contact(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
|
||||
q_early = create_contact(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
|
||||
|
||||
results = Radio.unprocessed_terrain_qsos()
|
||||
results = Radio.unprocessed_terrain_contacts()
|
||||
ids = Enum.map(results, & &1.id)
|
||||
assert ids == [q_early.id, q_late.id]
|
||||
end
|
||||
|
|
@ -313,32 +313,32 @@ defmodule Microwaveprop.RadioTest do
|
|||
test "respects limit parameter" do
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
create_qso(%{qso_timestamp: ts})
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
assert length(Radio.unprocessed_terrain_qsos(3)) == 3
|
||||
assert length(Radio.unprocessed_terrain_contacts(3)) == 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "mark_terrain_queued!/1" do
|
||||
test "sets terrain_queued to true for given IDs" do
|
||||
q1 = create_qso(%{station1: "A1A"})
|
||||
q2 = create_qso(%{station1: "B2B"})
|
||||
q1 = create_contact(%{station1: "A1A"})
|
||||
q2 = create_contact(%{station1: "B2B"})
|
||||
|
||||
Radio.mark_terrain_queued!([q1.id, q2.id])
|
||||
|
||||
assert Repo.get!(Qso, q1.id).terrain_queued == true
|
||||
assert Repo.get!(Qso, q2.id).terrain_queued == true
|
||||
assert Repo.get!(Contact, q1.id).terrain_queued == true
|
||||
assert Repo.get!(Contact, q2.id).terrain_queued == true
|
||||
end
|
||||
|
||||
test "does not affect other QSOs" do
|
||||
q1 = create_qso(%{station1: "A1A"})
|
||||
q2 = create_qso(%{station1: "B2B"})
|
||||
q1 = create_contact(%{station1: "A1A"})
|
||||
q2 = create_contact(%{station1: "B2B"})
|
||||
|
||||
Radio.mark_terrain_queued!([q1.id])
|
||||
|
||||
assert Repo.get!(Qso, q1.id).terrain_queued == true
|
||||
assert Repo.get!(Qso, q2.id).terrain_queued == false
|
||||
assert Repo.get!(Contact, q1.id).terrain_queued == true
|
||||
assert Repo.get!(Contact, q2.id).terrain_queued == false
|
||||
end
|
||||
|
||||
test "handles empty list" do
|
||||
|
|
@ -346,15 +346,15 @@ defmodule Microwaveprop.RadioTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "qso_path_points/1" do
|
||||
describe "contact_path_points/1" do
|
||||
test "returns pos1, midpoint, and pos2 when both positions exist" do
|
||||
qso =
|
||||
create_qso(%{
|
||||
contact =
|
||||
create_contact(%{
|
||||
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
||||
pos2: %{"lat" => 30.3, "lon" => -97.7}
|
||||
})
|
||||
|
||||
points = Radio.qso_path_points(qso)
|
||||
points = Radio.contact_path_points(contact)
|
||||
|
||||
assert length(points) == 3
|
||||
[{lat1, lon1}, {mid_lat, mid_lon}, {lat2, lon2}] = points
|
||||
|
|
@ -367,44 +367,44 @@ defmodule Microwaveprop.RadioTest do
|
|||
end
|
||||
|
||||
test "returns only pos1 when pos2 is nil" do
|
||||
qso = create_qso(%{pos2: nil})
|
||||
contact = create_contact(%{pos2: nil})
|
||||
|
||||
points = Radio.qso_path_points(qso)
|
||||
points = Radio.contact_path_points(contact)
|
||||
|
||||
assert points == [{32.9, -97.0}]
|
||||
end
|
||||
|
||||
test "returns empty list when pos1 is nil" do
|
||||
qso = create_qso(%{pos1: nil})
|
||||
contact = create_contact(%{pos1: nil})
|
||||
|
||||
assert Radio.qso_path_points(qso) == []
|
||||
assert Radio.contact_path_points(contact) == []
|
||||
end
|
||||
|
||||
test "handles lng key in position maps" do
|
||||
qso =
|
||||
create_qso(%{
|
||||
contact =
|
||||
create_contact(%{
|
||||
pos1: %{"lat" => 32.9, "lng" => -97.0},
|
||||
pos2: %{"lat" => 30.3, "lng" => -97.7}
|
||||
})
|
||||
|
||||
points = Radio.qso_path_points(qso)
|
||||
points = Radio.contact_path_points(contact)
|
||||
assert length(points) == 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_qso/2" do
|
||||
describe "change_contact/2" do
|
||||
test "returns a submission changeset" do
|
||||
changeset = Radio.change_qso(%Qso{})
|
||||
changeset = Radio.change_contact(%Contact{})
|
||||
assert %Ecto.Changeset{} = changeset
|
||||
end
|
||||
|
||||
test "accepts attrs" do
|
||||
changeset = Radio.change_qso(%Qso{}, %{station1: "W5XD"})
|
||||
changeset = Radio.change_contact(%Contact{}, %{station1: "W5XD"})
|
||||
assert Ecto.Changeset.get_change(changeset, :station1) == "W5XD"
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_qso/1" do
|
||||
describe "create_contact/1" do
|
||||
@valid_submission %{
|
||||
station1: "W5XD",
|
||||
station2: "K5TR",
|
||||
|
|
@ -417,38 +417,38 @@ defmodule Microwaveprop.RadioTest do
|
|||
}
|
||||
|
||||
test "creates QSO with computed positions and distance" do
|
||||
assert {:ok, qso} = Radio.create_qso(@valid_submission)
|
||||
assert {:ok, contact} = Radio.create_contact(@valid_submission)
|
||||
|
||||
assert qso.station1 == "W5XD"
|
||||
assert qso.user_submitted == true
|
||||
assert qso.pos1["lat"]
|
||||
assert qso.pos1["lon"]
|
||||
assert qso.pos2["lat"]
|
||||
assert qso.pos2["lon"]
|
||||
assert qso.distance_km
|
||||
assert contact.station1 == "W5XD"
|
||||
assert contact.user_submitted == true
|
||||
assert contact.pos1["lat"]
|
||||
assert contact.pos1["lon"]
|
||||
assert contact.pos2["lat"]
|
||||
assert contact.pos2["lon"]
|
||||
assert contact.distance_km
|
||||
end
|
||||
|
||||
test "computes correct position from grid" do
|
||||
assert {:ok, qso} = Radio.create_qso(@valid_submission)
|
||||
assert {:ok, contact} = Radio.create_contact(@valid_submission)
|
||||
|
||||
# EM12 center: lat = 4*10 - 90 + 1*1 + 0.5 = -48.5... wait
|
||||
# E=4, M=12 -> lon = 4*20 - 180 + 1*2 + 1 = -97, lat = 12*10 - 90 + 2*1 + 0.5 = 32.5
|
||||
assert_in_delta qso.pos1["lat"], 32.5, 0.1
|
||||
assert_in_delta qso.pos1["lon"], -97.0, 0.1
|
||||
assert_in_delta contact.pos1["lat"], 32.5, 0.1
|
||||
assert_in_delta contact.pos1["lon"], -97.0, 0.1
|
||||
end
|
||||
|
||||
test "sets user_submitted to true" do
|
||||
assert {:ok, qso} = Radio.create_qso(@valid_submission)
|
||||
assert qso.user_submitted == true
|
||||
assert {:ok, contact} = Radio.create_contact(@valid_submission)
|
||||
assert contact.user_submitted == true
|
||||
end
|
||||
|
||||
test "returns error changeset for invalid data" do
|
||||
assert {:error, %Ecto.Changeset{}} = Radio.create_qso(%{})
|
||||
assert {:error, %Ecto.Changeset{}} = Radio.create_contact(%{})
|
||||
end
|
||||
|
||||
test "returns error changeset for invalid grid" do
|
||||
attrs = Map.put(@valid_submission, :grid1, "ZZ99")
|
||||
assert {:error, changeset} = Radio.create_qso(attrs)
|
||||
assert {:error, changeset} = Radio.create_contact(attrs)
|
||||
assert errors_on(changeset).grid1
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Microwaveprop.Terrain.TerrainProfileTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Terrain.TerrainProfile
|
||||
|
||||
@qso_attrs %{
|
||||
|
|
@ -14,21 +14,21 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
|
|||
pos2: %{"lat" => 30.3, "lon" => -97.7}
|
||||
}
|
||||
|
||||
defp create_qso do
|
||||
{:ok, qso} =
|
||||
%Qso{}
|
||||
|> Qso.changeset(@qso_attrs)
|
||||
defp create_contact do
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(@qso_attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
qso
|
||||
contact
|
||||
end
|
||||
|
||||
describe "changeset/2" do
|
||||
test "valid with required fields" do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
attrs = %{
|
||||
qso_id: qso.id,
|
||||
qso_id: contact.id,
|
||||
sample_count: 65,
|
||||
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
|
||||
verdict: "CLEAR"
|
||||
|
|
@ -50,10 +50,10 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
|
|||
end
|
||||
|
||||
test "valid with all optional fields" do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
attrs = %{
|
||||
qso_id: qso.id,
|
||||
qso_id: contact.id,
|
||||
sample_count: 65,
|
||||
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
|
||||
verdict: "BLOCKED",
|
||||
|
|
@ -69,10 +69,10 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
|
|||
end
|
||||
|
||||
test "enforces unique qso_id constraint on insert" do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
attrs = %{
|
||||
qso_id: qso.id,
|
||||
qso_id: contact.id,
|
||||
sample_count: 65,
|
||||
path_points: [],
|
||||
verdict: "CLEAR"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Microwaveprop.TerrainTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Terrain
|
||||
|
||||
@qso_attrs %{
|
||||
|
|
@ -14,18 +14,18 @@ defmodule Microwaveprop.TerrainTest do
|
|||
pos2: %{"lat" => 30.3, "lon" => -97.7}
|
||||
}
|
||||
|
||||
defp create_qso do
|
||||
{:ok, qso} =
|
||||
%Qso{}
|
||||
|> Qso.changeset(@qso_attrs)
|
||||
defp create_contact do
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(@qso_attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
qso
|
||||
contact
|
||||
end
|
||||
|
||||
defp terrain_attrs(qso) do
|
||||
defp terrain_attrs(contact) do
|
||||
%{
|
||||
qso_id: qso.id,
|
||||
qso_id: contact.id,
|
||||
sample_count: 65,
|
||||
path_points: [
|
||||
%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0},
|
||||
|
|
@ -42,18 +42,18 @@ defmodule Microwaveprop.TerrainTest do
|
|||
|
||||
describe "upsert_terrain_profile/1" do
|
||||
test "inserts a new terrain profile" do
|
||||
qso = create_qso()
|
||||
attrs = terrain_attrs(qso)
|
||||
contact = create_contact()
|
||||
attrs = terrain_attrs(contact)
|
||||
|
||||
assert {:ok, profile} = Terrain.upsert_terrain_profile(attrs)
|
||||
assert profile.qso_id == qso.id
|
||||
assert profile.qso_id == contact.id
|
||||
assert profile.sample_count == 65
|
||||
assert profile.verdict == "CLEAR"
|
||||
end
|
||||
|
||||
test "upserts on conflict (same qso_id)" do
|
||||
qso = create_qso()
|
||||
attrs = terrain_attrs(qso)
|
||||
contact = create_contact()
|
||||
attrs = terrain_attrs(contact)
|
||||
|
||||
{:ok, _} = Terrain.upsert_terrain_profile(attrs)
|
||||
|
||||
|
|
@ -67,30 +67,30 @@ defmodule Microwaveprop.TerrainTest do
|
|||
|
||||
describe "get_terrain_profile/1" do
|
||||
test "returns profile for a QSO" do
|
||||
qso = create_qso()
|
||||
{:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(qso))
|
||||
contact = create_contact()
|
||||
{:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(contact))
|
||||
|
||||
profile = Terrain.get_terrain_profile(qso.id)
|
||||
assert profile.qso_id == qso.id
|
||||
profile = Terrain.get_terrain_profile(contact.id)
|
||||
assert profile.qso_id == contact.id
|
||||
end
|
||||
|
||||
test "returns nil when no profile exists" do
|
||||
qso = create_qso()
|
||||
assert Terrain.get_terrain_profile(qso.id) == nil
|
||||
contact = create_contact()
|
||||
assert Terrain.get_terrain_profile(contact.id) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "has_terrain_profile?/1" do
|
||||
test "returns true when profile exists" do
|
||||
qso = create_qso()
|
||||
{:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(qso))
|
||||
contact = create_contact()
|
||||
{:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(contact))
|
||||
|
||||
assert Terrain.has_terrain_profile?(qso.id)
|
||||
assert Terrain.has_terrain_profile?(contact.id)
|
||||
end
|
||||
|
||||
test "returns false when no profile exists" do
|
||||
qso = create_qso()
|
||||
refute Terrain.has_terrain_profile?(qso.id)
|
||||
contact = create_contact()
|
||||
refute Terrain.has_terrain_profile?(contact.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "weather_for_qso/2" do
|
||||
describe "weather_for_contact/2" do
|
||||
test "returns nearby surface observations within time window with station preloaded" do
|
||||
{:ok, station} = Weather.find_or_create_station(@station_attrs)
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
|
||||
# QSO near the station in space and time
|
||||
result =
|
||||
Weather.weather_for_qso(
|
||||
Weather.weather_for_contact(
|
||||
%{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 18:30:00Z]},
|
||||
radius_km: 100,
|
||||
time_window_hours: 3
|
||||
|
|
@ -149,7 +149,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
})
|
||||
|
||||
result =
|
||||
Weather.weather_for_qso(
|
||||
Weather.weather_for_contact(
|
||||
%{lat: 32.85, lon: -97.28, timestamp: ~U[2026-03-28 14:00:00Z]},
|
||||
radius_km: 100,
|
||||
time_window_hours: 6
|
||||
|
|
@ -170,7 +170,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
|
||||
# QSO far away
|
||||
result =
|
||||
Weather.weather_for_qso(
|
||||
Weather.weather_for_contact(
|
||||
%{lat: 40.0, lon: -80.0, timestamp: ~U[2026-03-28 18:00:00Z]},
|
||||
radius_km: 100,
|
||||
time_window_hours: 3
|
||||
|
|
@ -190,7 +190,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
|
||||
# QSO much later
|
||||
result =
|
||||
Weather.weather_for_qso(
|
||||
Weather.weather_for_contact(
|
||||
%{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 22:00:00Z]},
|
||||
radius_km: 100,
|
||||
time_window_hours: 3
|
||||
|
|
@ -482,31 +482,31 @@ defmodule Microwaveprop.WeatherTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "hrrr_for_qso/1" do
|
||||
describe "hrrr_for_contact/1" do
|
||||
test "returns nearest HRRR profile for a QSO" do
|
||||
Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
|
||||
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 32.91, "lon" => -97.05},
|
||||
qso_timestamp: ~U[2026-03-28 18:30:00Z]
|
||||
}
|
||||
|
||||
profile = Weather.hrrr_for_qso(qso)
|
||||
profile = Weather.hrrr_for_contact(contact)
|
||||
assert profile
|
||||
assert profile.lat == 32.90
|
||||
end
|
||||
|
||||
test "returns nil when no HRRR profiles exist nearby" do
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 40.0, "lon" => -80.0},
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z]
|
||||
}
|
||||
|
||||
assert Weather.hrrr_for_qso(qso) == nil
|
||||
assert Weather.hrrr_for_contact(contact) == nil
|
||||
end
|
||||
|
||||
test "returns nil when QSO has no pos1" do
|
||||
assert Weather.hrrr_for_qso(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
|
||||
assert Weather.hrrr_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -590,36 +590,36 @@ defmodule Microwaveprop.WeatherTest do
|
|||
lon: -97.70
|
||||
})
|
||||
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 32.91, "lon" => -97.05},
|
||||
pos2: %{"lat" => 30.31, "lon" => -97.71},
|
||||
qso_timestamp: ~U[2026-03-28 18:30:00Z]
|
||||
}
|
||||
|
||||
profiles = Weather.hrrr_profiles_for_path(qso)
|
||||
profiles = Weather.hrrr_profiles_for_path(contact)
|
||||
assert length(profiles) >= 2
|
||||
end
|
||||
|
||||
test "returns empty list when no HRRR profiles exist nearby" do
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 40.0, "lon" => -80.0},
|
||||
pos2: %{"lat" => 41.0, "lon" => -81.0},
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z]
|
||||
}
|
||||
|
||||
assert Weather.hrrr_profiles_for_path(qso) == []
|
||||
assert Weather.hrrr_profiles_for_path(contact) == []
|
||||
end
|
||||
|
||||
test "returns profiles for pos1 only when pos2 is nil" do
|
||||
Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
|
||||
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 32.91, "lon" => -97.05},
|
||||
pos2: nil,
|
||||
qso_timestamp: ~U[2026-03-28 18:30:00Z]
|
||||
}
|
||||
|
||||
profiles = Weather.hrrr_profiles_for_path(qso)
|
||||
profiles = Weather.hrrr_profiles_for_path(contact)
|
||||
assert length(profiles) == 1
|
||||
end
|
||||
end
|
||||
|
|
@ -636,36 +636,36 @@ defmodule Microwaveprop.WeatherTest do
|
|||
hourly: [%{"hour" => 0, "p01m_mm" => 1.0}]
|
||||
})
|
||||
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
||||
pos2: %{"lat" => 30.3, "lon" => -97.7},
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z]
|
||||
}
|
||||
|
||||
observations = Weather.iemre_for_path(qso)
|
||||
observations = Weather.iemre_for_path(contact)
|
||||
assert length(observations) >= 2
|
||||
end
|
||||
|
||||
test "returns empty list when no observations exist" do
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 40.0, "lon" => -80.0},
|
||||
pos2: %{"lat" => 41.0, "lon" => -81.0},
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z]
|
||||
}
|
||||
|
||||
assert Weather.iemre_for_path(qso) == []
|
||||
assert Weather.iemre_for_path(contact) == []
|
||||
end
|
||||
|
||||
test "returns observation for pos1 only when pos2 is nil" do
|
||||
Weather.upsert_iemre_observation(@iemre_attrs)
|
||||
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
||||
pos2: nil,
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z]
|
||||
}
|
||||
|
||||
observations = Weather.iemre_for_path(qso)
|
||||
observations = Weather.iemre_for_path(contact)
|
||||
assert length(observations) == 1
|
||||
end
|
||||
end
|
||||
|
|
@ -684,31 +684,31 @@ defmodule Microwaveprop.WeatherTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "iemre_for_qso/1" do
|
||||
describe "iemre_for_contact/1" do
|
||||
test "returns IEMRE observation matching QSO pos1 and date" do
|
||||
Weather.upsert_iemre_observation(@iemre_attrs)
|
||||
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z]
|
||||
}
|
||||
|
||||
obs = Weather.iemre_for_qso(qso)
|
||||
obs = Weather.iemre_for_contact(contact)
|
||||
assert obs
|
||||
assert obs.lat == 32.875
|
||||
end
|
||||
|
||||
test "returns nil when no IEMRE observation exists" do
|
||||
qso = %{
|
||||
contact = %{
|
||||
pos1: %{"lat" => 40.0, "lon" => -80.0},
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z]
|
||||
}
|
||||
|
||||
assert Weather.iemre_for_qso(qso) == nil
|
||||
assert Weather.iemre_for_contact(contact) == nil
|
||||
end
|
||||
|
||||
test "returns nil when QSO has no pos1" do
|
||||
assert Weather.iemre_for_qso(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
|
||||
assert Weather.iemre_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
||||
defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Terrain.ElevationClient
|
||||
alias Microwaveprop.Weather
|
||||
alias Microwaveprop.Weather.HrrrClient
|
||||
alias Microwaveprop.Weather.IemClient
|
||||
alias Microwaveprop.Workers.QsoWeatherEnqueueWorker
|
||||
alias Microwaveprop.Workers.ContactWeatherEnqueueWorker
|
||||
|
||||
@qso_attrs %{
|
||||
station1: "W5XD",
|
||||
|
|
@ -21,13 +21,13 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
defp create_qso(attrs \\ %{}) do
|
||||
{:ok, qso} =
|
||||
%Qso{}
|
||||
|> Qso.changeset(Map.merge(@qso_attrs, attrs))
|
||||
defp create_contact(attrs \\ %{}) do
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(Map.merge(@qso_attrs, attrs))
|
||||
|> Repo.insert()
|
||||
|
||||
qso
|
||||
contact
|
||||
end
|
||||
|
||||
defp create_asos_station(code, lat, lon) do
|
||||
|
|
@ -59,9 +59,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
describe "build_weather_jobs/1" do
|
||||
test "builds ASOS jobs for nearby stations" do
|
||||
station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
|
||||
|
||||
asos_jobs =
|
||||
Enum.filter(jobs, fn j ->
|
||||
|
|
@ -79,9 +79,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "builds RAOB jobs for nearby sounding stations" do
|
||||
station = create_sounding_station("FWD", 32.83, -97.30)
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
|
||||
|
||||
raob_jobs =
|
||||
Enum.filter(jobs, fn j ->
|
||||
|
|
@ -100,10 +100,10 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
_station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
|
||||
# Two QSOs at the exact same time produce identical args → deduplicated
|
||||
q1 = create_qso(%{station1: "A1", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
|
||||
q2 = create_qso(%{station1: "A2", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
|
||||
q1 = create_contact(%{station1: "A1", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
|
||||
q2 = create_contact(%{station1: "A2", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_weather_jobs([q1, q2])
|
||||
jobs = ContactWeatherEnqueueWorker.build_weather_jobs([q1, q2])
|
||||
|
||||
asos_jobs =
|
||||
Enum.filter(jobs, fn j ->
|
||||
|
|
@ -116,31 +116,31 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
test "builds ASOS jobs for stations near pos2 and midpoint" do
|
||||
# Station near pos2 (30.3, -97.7) but NOT near pos1 (32.9, -97.0)
|
||||
station_near_pos2 = create_asos_station("KAUS", 30.20, -97.68)
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
|
||||
|
||||
station_ids = Enum.map(jobs, & &1.changes.args["station_id"])
|
||||
assert station_near_pos2.id in station_ids
|
||||
end
|
||||
|
||||
test "returns empty list when no stations nearby" do
|
||||
qso = create_qso(%{pos1: %{"lat" => 60.0, "lon" => -150.0}, pos2: nil})
|
||||
contact = create_contact(%{pos1: %{"lat" => 60.0, "lon" => -150.0}, pos2: nil})
|
||||
|
||||
assert QsoWeatherEnqueueWorker.build_weather_jobs([qso]) == []
|
||||
assert ContactWeatherEnqueueWorker.build_weather_jobs([contact]) == []
|
||||
end
|
||||
|
||||
test "skips ASOS job when surface observations already exist for that station and time window" do
|
||||
station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
# Insert a surface observation within the time window (qso ± 2 hours)
|
||||
# Insert a surface observation within the time window (contact ± 2 hours)
|
||||
Weather.upsert_surface_observation(station, %{
|
||||
observed_at: ~U[2026-03-28 18:00:00Z],
|
||||
temp_f: 75.0
|
||||
})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
|
||||
|
||||
asos_jobs =
|
||||
Enum.filter(jobs, fn j ->
|
||||
|
|
@ -152,10 +152,10 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "skips RAOB job when sounding already exists for that station and time" do
|
||||
station = create_sounding_station("FWD", 32.83, -97.30)
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
# Insert soundings for both bracketing times (00Z and 12Z on 2026-03-28)
|
||||
for time <- Weather.sounding_times_around(qso.qso_timestamp) do
|
||||
for time <- Weather.sounding_times_around(contact.qso_timestamp) do
|
||||
Weather.upsert_sounding(station, %{
|
||||
observed_at: time,
|
||||
profile: [%{"pres" => 1013.0, "tmpc" => 25.0}],
|
||||
|
|
@ -163,7 +163,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
})
|
||||
end
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
|
||||
|
||||
raob_jobs =
|
||||
Enum.filter(jobs, fn j ->
|
||||
|
|
@ -207,58 +207,58 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "enqueues weather jobs and marks QSOs as queued" do
|
||||
_station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
assert qso.weather_queued == false
|
||||
assert contact.weather_queued == false
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.weather_queued == true
|
||||
end
|
||||
|
||||
test "returns :ok when no unprocessed QSOs exist" do
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "backfills distance_km for QSOs missing it" do
|
||||
_station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
qso = create_qso(%{distance_km: nil})
|
||||
contact = create_contact(%{distance_km: nil})
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.distance_km
|
||||
assert_in_delta Decimal.to_float(updated.distance_km), 295, 5
|
||||
end
|
||||
|
||||
test "skips QSOs without positions" do
|
||||
_station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
_qso = create_qso(%{pos1: nil})
|
||||
_qso = create_contact(%{pos1: nil})
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# QSO without pos1 should not be marked as queued
|
||||
all_qsos = Repo.all(Qso)
|
||||
all_qsos = Repo.all(Contact)
|
||||
refute Enum.any?(all_qsos, & &1.weather_queued)
|
||||
end
|
||||
|
||||
test "enqueues HRRR jobs and marks QSOs as hrrr_queued" do
|
||||
qso = create_qso()
|
||||
assert qso.hrrr_queued == false
|
||||
contact = create_contact()
|
||||
assert contact.hrrr_queued == false
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.hrrr_queued == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "build_hrrr_jobs/1" do
|
||||
test "batches all path points into one job per HRRR hour" do
|
||||
qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
|
||||
|
||||
# All 3 path points share the same HRRR hour → 1 batch job
|
||||
assert length(jobs) == 1
|
||||
|
|
@ -271,9 +271,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
end
|
||||
|
||||
test "batch contains single point when pos2 is nil" do
|
||||
qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
|
||||
contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
|
||||
|
||||
assert length(jobs) == 1
|
||||
job = hd(jobs)
|
||||
|
|
@ -283,9 +283,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
end
|
||||
|
||||
test "rounds valid_time to nearest hour" do
|
||||
qso = create_qso(%{qso_timestamp: ~U[2026-03-28 18:45:00Z], pos2: nil})
|
||||
contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:45:00Z], pos2: nil})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
|
||||
|
||||
job = hd(jobs)
|
||||
assert job.changes.args["valid_time"] == "2026-03-28T19:00:00Z"
|
||||
|
|
@ -293,7 +293,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "deduplicates jobs across QSOs with identical path points" do
|
||||
q1 =
|
||||
create_qso(%{
|
||||
create_contact(%{
|
||||
station1: "A1",
|
||||
qso_timestamp: ~U[2026-03-28 18:10:00Z],
|
||||
pos1: %{"lat" => 32.90, "lon" => -97.04},
|
||||
|
|
@ -301,14 +301,14 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
})
|
||||
|
||||
q2 =
|
||||
create_qso(%{
|
||||
create_contact(%{
|
||||
station1: "A2",
|
||||
qso_timestamp: ~U[2026-03-28 18:20:00Z],
|
||||
pos1: %{"lat" => 32.90, "lon" => -97.04},
|
||||
pos2: nil
|
||||
})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([q1, q2])
|
||||
jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([q1, q2])
|
||||
|
||||
# Both round to same hour and same location → 1 job
|
||||
assert length(jobs) == 1
|
||||
|
|
@ -316,26 +316,26 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "deduplicates path points that round to the same HRRR grid cell" do
|
||||
# pos1 and pos2 only 0.001° apart — all 3 points round to same HRRR cell
|
||||
qso =
|
||||
create_qso(%{
|
||||
contact =
|
||||
create_contact(%{
|
||||
pos1: %{"lat" => 32.901, "lon" => -97.041},
|
||||
pos2: %{"lat" => 32.902, "lon" => -97.042}
|
||||
})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
|
||||
|
||||
# All 3 path points round to {32.90, -97.04} → 1 job
|
||||
assert length(jobs) == 1
|
||||
end
|
||||
|
||||
test "skips QSOs without pos1" do
|
||||
qso = create_qso(%{pos1: nil})
|
||||
contact = create_contact(%{pos1: nil})
|
||||
|
||||
assert QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) == []
|
||||
assert ContactWeatherEnqueueWorker.build_hrrr_jobs([contact]) == []
|
||||
end
|
||||
|
||||
test "excludes existing points from batch" do
|
||||
qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
|
||||
# Insert a profile at the pos1 rounded grid point only
|
||||
Weather.upsert_hrrr_profile(%{
|
||||
|
|
@ -351,7 +351,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
surface_pressure_mb: 1013.0
|
||||
})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
|
||||
|
||||
# 1 batch job, but pos1 excluded from points list
|
||||
assert length(jobs) == 1
|
||||
|
|
@ -364,19 +364,19 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
describe "build_terrain_jobs/1" do
|
||||
test "builds one terrain job per QSO" do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_terrain_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact])
|
||||
|
||||
assert length(jobs) == 1
|
||||
job = hd(jobs)
|
||||
assert job.changes.args["qso_id"] == qso.id
|
||||
assert job.changes.args["qso_id"] == contact.id
|
||||
end
|
||||
|
||||
test "deduplicates by qso_id" do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_terrain_jobs([qso, qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact, contact])
|
||||
assert length(jobs) == 1
|
||||
end
|
||||
end
|
||||
|
|
@ -410,30 +410,30 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
end
|
||||
|
||||
test "enqueues terrain jobs and marks QSOs as terrain_queued" do
|
||||
qso = create_qso()
|
||||
assert qso.terrain_queued == false
|
||||
contact = create_contact()
|
||||
assert contact.terrain_queued == false
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.terrain_queued == true
|
||||
end
|
||||
|
||||
test "skips terrain for QSOs missing pos2" do
|
||||
_qso = create_qso(%{pos2: nil})
|
||||
_qso = create_contact(%{pos2: nil})
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
all_qsos = Repo.all(Qso)
|
||||
all_qsos = Repo.all(Contact)
|
||||
refute Enum.any?(all_qsos, & &1.terrain_queued)
|
||||
end
|
||||
end
|
||||
|
||||
describe "build_iemre_jobs/1" do
|
||||
test "builds IEMRE jobs for all path points (pos1, midpoint, pos2)" do
|
||||
qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
|
||||
|
||||
# pos1, midpoint, pos2 may have distinct IEMRE grid cells
|
||||
assert length(jobs) >= 2
|
||||
|
|
@ -442,9 +442,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
end
|
||||
|
||||
test "builds only one IEMRE job when pos2 is nil" do
|
||||
qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
|
||||
contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
|
||||
|
||||
assert length(jobs) == 1
|
||||
job = hd(jobs)
|
||||
|
|
@ -455,7 +455,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "deduplicates jobs across QSOs with identical path points" do
|
||||
q1 =
|
||||
create_qso(%{
|
||||
create_contact(%{
|
||||
station1: "A1",
|
||||
qso_timestamp: ~U[2026-03-28 18:10:00Z],
|
||||
pos1: %{"lat" => 32.90, "lon" => -97.04},
|
||||
|
|
@ -463,14 +463,14 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
})
|
||||
|
||||
q2 =
|
||||
create_qso(%{
|
||||
create_contact(%{
|
||||
station1: "A2",
|
||||
qso_timestamp: ~U[2026-03-28 20:20:00Z],
|
||||
pos1: %{"lat" => 32.90, "lon" => -97.04},
|
||||
pos2: nil
|
||||
})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
|
||||
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
|
||||
|
||||
# Same location and same date → 1 job
|
||||
assert length(jobs) == 1
|
||||
|
|
@ -478,7 +478,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "creates separate jobs for different dates" do
|
||||
q1 =
|
||||
create_qso(%{
|
||||
create_contact(%{
|
||||
station1: "A1",
|
||||
qso_timestamp: ~U[2026-03-28 23:00:00Z],
|
||||
pos1: %{"lat" => 32.90, "lon" => -97.04},
|
||||
|
|
@ -486,24 +486,24 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
})
|
||||
|
||||
q2 =
|
||||
create_qso(%{
|
||||
create_contact(%{
|
||||
station1: "A2",
|
||||
qso_timestamp: ~U[2026-03-29 01:00:00Z],
|
||||
pos1: %{"lat" => 32.90, "lon" => -97.04},
|
||||
pos2: nil
|
||||
})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
|
||||
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
|
||||
assert length(jobs) == 2
|
||||
end
|
||||
|
||||
test "skips QSOs without pos1" do
|
||||
qso = create_qso(%{pos1: nil})
|
||||
assert QsoWeatherEnqueueWorker.build_iemre_jobs([qso]) == []
|
||||
contact = create_contact(%{pos1: nil})
|
||||
assert ContactWeatherEnqueueWorker.build_iemre_jobs([contact]) == []
|
||||
end
|
||||
|
||||
test "skips only path points where IEMRE observation already exists" do
|
||||
qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
|
||||
|
||||
# Insert an IEMRE observation at the pos1 rounded grid point only
|
||||
Weather.upsert_iemre_observation(%{
|
||||
|
|
@ -513,7 +513,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
hourly: [%{"hour" => 0, "p01m_mm" => 0.0}]
|
||||
})
|
||||
|
||||
jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso])
|
||||
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
|
||||
|
||||
# pos1 skipped, midpoint + pos2 still need fetching
|
||||
assert length(jobs) >= 1
|
||||
|
|
@ -551,17 +551,17 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
end
|
||||
|
||||
test "enqueues IEMRE jobs and marks QSOs as iemre_queued" do
|
||||
qso = create_qso()
|
||||
assert qso.iemre_queued == false
|
||||
contact = create_contact()
|
||||
assert contact.iemre_queued == false
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.iemre_queued == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "enqueue_for_qso/1" do
|
||||
describe "enqueue_for_contact/1" do
|
||||
setup do
|
||||
Req.Test.stub(IemClient, fn conn ->
|
||||
case conn.request_path do
|
||||
|
|
@ -591,16 +591,16 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "enqueues all enrichment jobs and marks QSO as fully queued" do
|
||||
_station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
assert qso.weather_queued == false
|
||||
assert qso.hrrr_queued == false
|
||||
assert qso.terrain_queued == false
|
||||
assert qso.iemre_queued == false
|
||||
assert contact.weather_queued == false
|
||||
assert contact.hrrr_queued == false
|
||||
assert contact.terrain_queued == false
|
||||
assert contact.iemre_queued == false
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.enqueue_for_qso(qso)
|
||||
assert :ok = ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.weather_queued == true
|
||||
assert updated.hrrr_queued == true
|
||||
assert updated.terrain_queued == true
|
||||
|
|
@ -609,11 +609,11 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
|
|||
|
||||
test "works for QSO without pos2 (marks terrain_queued false)" do
|
||||
_station = create_asos_station("KDFW", 32.90, -97.04)
|
||||
qso = create_qso(%{pos2: nil})
|
||||
contact = create_contact(%{pos2: nil})
|
||||
|
||||
assert :ok = QsoWeatherEnqueueWorker.enqueue_for_qso(qso)
|
||||
assert :ok = ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.weather_queued == true
|
||||
assert updated.hrrr_queued == true
|
||||
# terrain requires pos2, so it can't be queued
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Terrain
|
||||
alias Microwaveprop.Terrain.ElevationClient
|
||||
alias Microwaveprop.Workers.TerrainProfileWorker
|
||||
|
|
@ -17,13 +17,13 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
|
|||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
defp create_qso(attrs \\ %{}) do
|
||||
{:ok, qso} =
|
||||
%Qso{}
|
||||
|> Qso.changeset(Map.merge(@qso_attrs, attrs))
|
||||
defp create_contact(attrs \\ %{}) do
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(Map.merge(@qso_attrs, attrs))
|
||||
|> Repo.insert()
|
||||
|
||||
qso
|
||||
contact
|
||||
end
|
||||
|
||||
defp stub_elevation_api do
|
||||
|
|
@ -38,15 +38,15 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
|
|||
describe "perform/1" do
|
||||
test "fetches elevation and stores terrain profile" do
|
||||
stub_elevation_api()
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
refute Terrain.has_terrain_profile?(qso.id)
|
||||
refute Terrain.has_terrain_profile?(contact.id)
|
||||
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
|
||||
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
|
||||
|
||||
assert Terrain.has_terrain_profile?(qso.id)
|
||||
profile = Terrain.get_terrain_profile(qso.id)
|
||||
assert Terrain.has_terrain_profile?(contact.id)
|
||||
profile = Terrain.get_terrain_profile(contact.id)
|
||||
assert profile.sample_count == 65
|
||||
assert profile.verdict in ["CLEAR", "FRESNEL_MINOR", "FRESNEL_PARTIAL", "BLOCKED"]
|
||||
assert is_list(profile.path_points)
|
||||
|
|
@ -54,17 +54,17 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
|
|||
|
||||
test "skips if terrain profile already exists" do
|
||||
stub_elevation_api()
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
{:ok, _} =
|
||||
Terrain.upsert_terrain_profile(%{
|
||||
qso_id: qso.id,
|
||||
qso_id: contact.id,
|
||||
sample_count: 65,
|
||||
path_points: [],
|
||||
verdict: "CLEAR"
|
||||
})
|
||||
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
|
||||
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
|
||||
end
|
||||
|
||||
|
|
@ -73,22 +73,22 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
|
|||
Plug.Conn.send_resp(conn, 500, "error")
|
||||
end)
|
||||
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
|
||||
assert {:error, _} = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
|
||||
|
||||
refute Terrain.has_terrain_profile?(qso.id)
|
||||
refute Terrain.has_terrain_profile?(contact.id)
|
||||
end
|
||||
|
||||
test "uses correct frequency from band" do
|
||||
stub_elevation_api()
|
||||
qso = create_qso(%{band: Decimal.new("10368")})
|
||||
contact = create_contact(%{band: Decimal.new("10368")})
|
||||
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
|
||||
job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
|
||||
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
|
||||
|
||||
profile = Terrain.get_terrain_profile(qso.id)
|
||||
profile = Terrain.get_terrain_profile(contact.id)
|
||||
assert profile.verdict
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
defmodule MicrowavepropWeb.QsoLiveTest do
|
||||
defmodule MicrowavepropWeb.ContactLiveTest do
|
||||
use MicrowavepropWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
defp create_qso(attrs \\ %{}) do
|
||||
defp create_contact(attrs \\ %{}) do
|
||||
default = %{
|
||||
station1: "W5XD",
|
||||
station2: "K5TR",
|
||||
|
|
@ -20,24 +20,24 @@ defmodule MicrowavepropWeb.QsoLiveTest do
|
|||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
{:ok, qso} =
|
||||
%Qso{}
|
||||
|> Qso.changeset(Map.merge(default, attrs))
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(Map.merge(default, attrs))
|
||||
|> Repo.insert()
|
||||
|
||||
qso
|
||||
contact
|
||||
end
|
||||
|
||||
describe "Index" do
|
||||
test "renders page with QSOs heading", %{conn: conn} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos")
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
||||
assert html =~ "QSOs"
|
||||
end
|
||||
|
||||
test "table shows QSO data", %{conn: conn} do
|
||||
create_qso()
|
||||
create_contact()
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos")
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
||||
assert html =~ "W5XD"
|
||||
assert html =~ "K5TR"
|
||||
assert html =~ "CW"
|
||||
|
|
@ -45,17 +45,17 @@ defmodule MicrowavepropWeb.QsoLiveTest do
|
|||
end
|
||||
|
||||
test "row links to detail page", %{conn: conn} do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos")
|
||||
assert html =~ ~p"/qsos/#{qso.id}"
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
||||
assert html =~ ~p"/contacts/#{contact.id}"
|
||||
end
|
||||
|
||||
test "sort params order the table", %{conn: conn} do
|
||||
create_qso(%{station1: "ZZ9ZZ"})
|
||||
create_qso(%{station1: "AA1AA"})
|
||||
create_contact(%{station1: "ZZ9ZZ"})
|
||||
create_contact(%{station1: "AA1AA"})
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos?sort_by=station1&sort_order=asc")
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts?sort_by=station1&sort_order=asc")
|
||||
|
||||
# AA1AA should appear before ZZ9ZZ
|
||||
aa_pos = html |> :binary.match("AA1AA") |> elem(0)
|
||||
|
|
@ -66,22 +66,22 @@ defmodule MicrowavepropWeb.QsoLiveTest do
|
|||
test "pagination works", %{conn: conn} do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
create_qso(%{qso_timestamp: ts})
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos")
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
||||
assert html =~ "Page 1 of 2"
|
||||
|
||||
{:ok, _lv, html2} = live(conn, ~p"/qsos?page=2")
|
||||
{:ok, _lv, html2} = live(conn, ~p"/contacts?page=2")
|
||||
assert html2 =~ "Page 2 of 2"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Show" do
|
||||
test "renders QSO detail fields", %{conn: conn} do
|
||||
qso = create_qso()
|
||||
contact = create_contact()
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
|
||||
assert html =~ "W5XD"
|
||||
assert html =~ "K5TR"
|
||||
assert html =~ "CW"
|
||||
|
|
@ -91,38 +91,38 @@ defmodule MicrowavepropWeb.QsoLiveTest do
|
|||
end
|
||||
|
||||
test "shows surface observations section", %{conn: conn} do
|
||||
qso = create_qso()
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
|
||||
contact = create_contact()
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
|
||||
assert html =~ "Surface Observations"
|
||||
end
|
||||
|
||||
test "shows sounding section", %{conn: conn} do
|
||||
qso = create_qso()
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
|
||||
contact = create_contact()
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
|
||||
assert html =~ "Soundings"
|
||||
end
|
||||
|
||||
test "shows solar index section", %{conn: conn} do
|
||||
qso = create_qso()
|
||||
{:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
|
||||
contact = create_contact()
|
||||
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
|
||||
assert html =~ "Solar Conditions"
|
||||
end
|
||||
|
||||
test "renders with sort assigns", %{conn: conn} do
|
||||
qso = create_qso()
|
||||
{:ok, lv, html} = live(conn, ~p"/qsos/#{qso.id}")
|
||||
contact = create_contact()
|
||||
{:ok, lv, html} = live(conn, ~p"/contacts/#{contact.id}")
|
||||
|
||||
# Page renders without error
|
||||
assert html =~ "Surface Observations"
|
||||
assert html =~ "Soundings"
|
||||
|
||||
# Sort assigns are initialized (no crash on render)
|
||||
assert render(lv) =~ qso.station1
|
||||
assert render(lv) =~ contact.station1
|
||||
end
|
||||
|
||||
test "raises for bad UUID", %{conn: conn} do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
live(conn, ~p"/qsos/#{Ecto.UUID.generate()}")
|
||||
live(conn, ~p"/contacts/#{Ecto.UUID.generate()}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
|
|||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Repo
|
||||
alias Microwaveprop.Terrain.ElevationClient
|
||||
alias Microwaveprop.Weather.HrrrClient
|
||||
|
|
@ -33,7 +33,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
|
|||
describe "mount" do
|
||||
test "renders the submission form", %{conn: conn} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/submit")
|
||||
assert html =~ "Submit QSO"
|
||||
assert html =~ "Submit Contact"
|
||||
assert html =~ "Station 1"
|
||||
assert html =~ "Station 2"
|
||||
assert html =~ "Grid 1"
|
||||
|
|
@ -49,7 +49,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
|
|||
|
||||
html =
|
||||
lv
|
||||
|> form("#qso-form", qso: %{station1: ""})
|
||||
|> form("#contact-form", contact: %{station1: ""})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "can't be blank"
|
||||
|
|
@ -61,8 +61,8 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
|
|||
{:ok, lv, _html} = live(conn, ~p"/submit")
|
||||
|
||||
lv
|
||||
|> form("#qso-form",
|
||||
qso: %{
|
||||
|> form("#contact-form",
|
||||
contact: %{
|
||||
station1: "W5XD",
|
||||
station2: "K5TR",
|
||||
qso_timestamp: "2026-03-28T18:00",
|
||||
|
|
@ -75,12 +75,12 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
|
|||
)
|
||||
|> render_submit()
|
||||
|
||||
qso = Repo.one!(Qso)
|
||||
assert qso.station1 == "W5XD"
|
||||
assert qso.user_submitted == true
|
||||
assert qso.pos1["lat"]
|
||||
contact = Repo.one!(Contact)
|
||||
assert contact.station1 == "W5XD"
|
||||
assert contact.user_submitted == true
|
||||
assert contact.pos1["lat"]
|
||||
|
||||
assert_redirect(lv, ~p"/qsos/#{qso.id}")
|
||||
assert_redirect(lv, ~p"/contacts/#{contact.id}")
|
||||
end
|
||||
|
||||
test "shows errors on invalid submit", %{conn: conn} do
|
||||
|
|
@ -88,7 +88,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
|
|||
|
||||
html =
|
||||
lv
|
||||
|> form("#qso-form", qso: %{station1: ""})
|
||||
|> form("#contact-form", contact: %{station1: ""})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "can't be blank"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Mix.Tasks.ResetEnrichmentTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Qso
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Mix.Tasks.ResetEnrichment
|
||||
|
||||
defp create_queued_qso do
|
||||
|
|
@ -20,41 +20,41 @@ defmodule Mix.Tasks.ResetEnrichmentTest do
|
|||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
{:ok, qso} =
|
||||
%Qso{}
|
||||
|> Qso.changeset(default)
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(default)
|
||||
|> Repo.insert()
|
||||
|
||||
# Set all queued flags to true (not in changeset cast)
|
||||
Qso
|
||||
|> where([q], q.id == ^qso.id)
|
||||
Contact
|
||||
|> where([q], q.id == ^contact.id)
|
||||
|> Repo.update_all(set: [weather_queued: true, hrrr_queued: true, iemre_queued: true, terrain_queued: true])
|
||||
|
||||
Repo.get!(Qso, qso.id)
|
||||
Repo.get!(Contact, contact.id)
|
||||
end
|
||||
|
||||
describe "run/1" do
|
||||
test "resets weather_queued, hrrr_queued, and iemre_queued to false" do
|
||||
qso = create_queued_qso()
|
||||
contact = create_queued_qso()
|
||||
|
||||
assert qso.weather_queued == true
|
||||
assert qso.hrrr_queued == true
|
||||
assert qso.iemre_queued == true
|
||||
assert contact.weather_queued == true
|
||||
assert contact.hrrr_queued == true
|
||||
assert contact.iemre_queued == true
|
||||
|
||||
ResetEnrichment.run([])
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.weather_queued == false
|
||||
assert updated.hrrr_queued == false
|
||||
assert updated.iemre_queued == false
|
||||
end
|
||||
|
||||
test "does not reset terrain_queued" do
|
||||
qso = create_queued_qso()
|
||||
contact = create_queued_qso()
|
||||
|
||||
ResetEnrichment.run([])
|
||||
|
||||
updated = Repo.get!(Qso, qso.id)
|
||||
updated = Repo.get!(Contact, contact.id)
|
||||
assert updated.terrain_queued == true
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue