feat(accounts): auto-prefill home QTH from QRZ on register + backfill on boot
This commit is contained in:
parent
b1fe077863
commit
9796f06ba2
6 changed files with 197 additions and 1 deletions
|
|
@ -48,6 +48,11 @@ config :microwaveprop, MicrowavepropWeb.Endpoint,
|
|||
# Run Oban jobs inline during tests
|
||||
config :microwaveprop, Oban, testing: :inline
|
||||
|
||||
# Skip the registration-time QRZ lookup in tests — it needs a Mox stub
|
||||
# that isn't set up across the test suite, and the worker is exercised
|
||||
# directly in its own dedicated test.
|
||||
config :microwaveprop, :enable_home_qth_lookup, false
|
||||
|
||||
# MetricsLogSuppressionTest hits GET /metrics and asserts no "Sent 200"
|
||||
# log line; that path needs PromEx alive to return a 200. We can't
|
||||
# blanket-disable PromEx. Instead, toggle off the Oban-queue poller
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ defmodule Microwaveprop.Accounts do
|
|||
alias Microwaveprop.Accounts.UserNotifier
|
||||
alias Microwaveprop.Accounts.UserToken
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
## Database getters
|
||||
alias Microwaveprop.Workers.UserHomeQthLookupWorker
|
||||
|
||||
@doc """
|
||||
Gets a user by email.
|
||||
|
|
@ -132,6 +132,47 @@ defmodule Microwaveprop.Accounts do
|
|||
%User{}
|
||||
|> User.registration_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
|> maybe_enqueue_home_qth_lookup()
|
||||
end
|
||||
|
||||
defp maybe_enqueue_home_qth_lookup({:ok, %User{id: id, callsign: call} = user}) when is_binary(call) do
|
||||
if home_qth_lookup_enabled?() do
|
||||
worker = UserHomeQthLookupWorker
|
||||
|
||||
if Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do
|
||||
_ = %{user_id: id} |> worker.new() |> Oban.insert()
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, user}
|
||||
end
|
||||
|
||||
defp maybe_enqueue_home_qth_lookup(other), do: other
|
||||
|
||||
defp home_qth_lookup_enabled? do
|
||||
Application.get_env(:microwaveprop, :enable_home_qth_lookup, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enqueue a `UserHomeQthLookupWorker` job for every user whose
|
||||
`home_grid` is still nil. Safe to call repeatedly — the worker
|
||||
no-ops when the user has set their QTH manually.
|
||||
"""
|
||||
@spec backfill_missing_home_qth() :: :ok
|
||||
def backfill_missing_home_qth do
|
||||
worker = UserHomeQthLookupWorker
|
||||
|
||||
if home_qth_lookup_enabled?() and Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do
|
||||
User
|
||||
|> where([u], is_nil(u.home_grid))
|
||||
|> select([u], u.id)
|
||||
|> Repo.all()
|
||||
|> Enum.each(fn id ->
|
||||
_ = %{user_id: id} |> worker.new() |> Oban.insert()
|
||||
end)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -60,6 +60,13 @@ defmodule Microwaveprop.Application do
|
|||
# lifetime of the pod.
|
||||
{:ok, _pid} = Task.start(fn -> Microwaveprop.Weather.warm_grid_cache_from_latest_profile() end)
|
||||
|
||||
# One-shot backfill: enqueue a QRZ-lookup job for every user with a
|
||||
# missing home QTH. Idempotent — the worker no-ops if home_grid is
|
||||
# already set, so re-running on every boot is safe (and useful: any
|
||||
# users registered while the worker module was unavailable get
|
||||
# picked up the next time the pod restarts).
|
||||
{:ok, _pid} = Task.start(fn -> Microwaveprop.Accounts.backfill_missing_home_qth() end)
|
||||
|
||||
# Load ML model in dev/test only (Nx/Axon not compiled for prod)
|
||||
if Application.get_env(:microwaveprop, :load_ml_model, false) do
|
||||
Microwaveprop.Propagation.load_ml_model()
|
||||
|
|
|
|||
70
lib/microwaveprop/workers/user_home_qth_lookup_worker.ex
Normal file
70
lib/microwaveprop/workers/user_home_qth_lookup_worker.ex
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
defmodule Microwaveprop.Workers.UserHomeQthLookupWorker do
|
||||
@moduledoc """
|
||||
Looks up a user's callsign via QRZ (with Google Maps fallback) and
|
||||
populates their home QTH fields if they're still empty.
|
||||
|
||||
Enqueued automatically on `Accounts.register_user/1` and reusable for
|
||||
backfilling existing users via:
|
||||
|
||||
Microwaveprop.Repo.all(
|
||||
from u in Microwaveprop.Accounts.User, where: is_nil(u.home_grid)
|
||||
)
|
||||
|> Enum.each(fn user ->
|
||||
%{user_id: user.id}
|
||||
|> Microwaveprop.Workers.UserHomeQthLookupWorker.new()
|
||||
|> Oban.insert()
|
||||
end)
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: :weather, max_attempts: 3
|
||||
|
||||
alias Microwaveprop.Accounts
|
||||
alias Microwaveprop.Accounts.User
|
||||
alias Microwaveprop.Radio.CallsignClient
|
||||
alias Microwaveprop.Radio.Maidenhead
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
|
||||
case Repo.get(User, user_id) do
|
||||
nil ->
|
||||
:ok
|
||||
|
||||
%User{home_grid: grid} when is_binary(grid) and grid != "" ->
|
||||
# User already set their home QTH manually; don't overwrite.
|
||||
:ok
|
||||
|
||||
%User{callsign: nil} ->
|
||||
:ok
|
||||
|
||||
%User{callsign: callsign} = user ->
|
||||
do_lookup(user, callsign)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_lookup(user, callsign) do
|
||||
case CallsignClient.locate(callsign) do
|
||||
{:ok, %{lat: lat, lon: lon} = result} ->
|
||||
grid = result[:gridsquare] || Maidenhead.from_latlon(lat, lon, 10)
|
||||
attrs = %{"home_grid" => grid, "home_lat" => lat, "home_lon" => lon}
|
||||
|
||||
case Accounts.update_user_home_qth(user, attrs) do
|
||||
{:ok, _user} ->
|
||||
:ok
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.warning(
|
||||
"UserHomeQthLookupWorker save failed: callsign=#{inspect(callsign)} errors=#{inspect(changeset.errors)}"
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.info("UserHomeQthLookupWorker lookup failed for #{inspect(callsign)}: #{inspect(reason)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -32,6 +32,17 @@
|
|||
Used to anchor the rover map (drive radius, distance/bearing labels).
|
||||
Enter a Maidenhead grid (e.g. <code>EM13qc</code>) or explicit lat/lon.
|
||||
</p>
|
||||
<div
|
||||
:if={!@current_scope.user.home_grid}
|
||||
class="alert alert-warning text-xs mt-2"
|
||||
>
|
||||
<.icon name="hero-exclamation-triangle" class="size-4 shrink-0" />
|
||||
<span>
|
||||
Using the default NTMS-area location (EM13).
|
||||
A QRZ lookup for <code>{@current_scope.user.callsign}</code>
|
||||
is queued — check back shortly, or set yours manually below.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.input
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
defmodule Microwaveprop.Workers.UserHomeQthLookupWorkerTest do
|
||||
use Microwaveprop.DataCase, async: false
|
||||
use Oban.Testing, repo: Microwaveprop.Repo
|
||||
|
||||
import Microwaveprop.AccountsFixtures
|
||||
|
||||
alias Microwaveprop.Repo
|
||||
alias Microwaveprop.Workers.UserHomeQthLookupWorker
|
||||
|
||||
# The test config disables enqueueing on registration globally so the
|
||||
# rest of the suite doesn't need a Mox stub. We don't toggle that here
|
||||
# — these tests invoke the worker directly via `perform_job/2`, which
|
||||
# is independent of the enqueue flag.
|
||||
|
||||
describe "perform/1" do
|
||||
test "no-ops when the user already has a home_grid set" do
|
||||
user =
|
||||
user_fixture()
|
||||
|> Ecto.Changeset.change(home_grid: "EM13qc", home_lat: 33.1, home_lon: -96.6)
|
||||
|> Repo.update!()
|
||||
|
||||
assert :ok = perform_job(UserHomeQthLookupWorker, %{user_id: user.id})
|
||||
reloaded = Repo.reload(user)
|
||||
assert reloaded.home_grid == "EM13qc"
|
||||
end
|
||||
|
||||
test "no-ops when the user no longer exists" do
|
||||
assert :ok = perform_job(UserHomeQthLookupWorker, %{user_id: Ecto.UUID.generate()})
|
||||
end
|
||||
|
||||
test "populates home QTH from a successful CallsignClient lookup" do
|
||||
user = user_fixture()
|
||||
|
||||
# Stub QRZ.com HTTP layer to return coords for the user's callsign.
|
||||
# The callsign-location pipeline reads either lat/lon or grid from
|
||||
# the QRZ XML; we serve coords directly so the geocoder fallback
|
||||
# isn't exercised.
|
||||
Req.Test.stub(Microwaveprop.Qrz.Client, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<QRZDatabase>
|
||||
<Session><Key>test-session</Key></Session>
|
||||
<Callsign>
|
||||
<call>#{user.callsign}</call>
|
||||
<lat>33.10</lat>
|
||||
<lon>-96.625</lon>
|
||||
<grid>EM13qc</grid>
|
||||
</Callsign>
|
||||
</QRZDatabase>
|
||||
""")
|
||||
end)
|
||||
|
||||
assert :ok = perform_job(UserHomeQthLookupWorker, %{user_id: user.id})
|
||||
|
||||
reloaded = Repo.reload(user)
|
||||
assert is_binary(reloaded.home_grid)
|
||||
assert String.starts_with?(reloaded.home_grid, "EM13qc")
|
||||
assert is_float(reloaded.home_lat)
|
||||
assert is_float(reloaded.home_lon)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue