Deduplicate single contact submissions same as CSV import
Check for existing contacts with the same station pair (order-independent), band, and grids within a 1-hour window before inserting.
This commit is contained in:
parent
3334db5a08
commit
cdb59be6d7
2 changed files with 87 additions and 12 deletions
|
|
@ -329,6 +329,8 @@ defmodule Microwaveprop.Radio do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@dedup_window_seconds 3600
|
||||||
|
|
||||||
def create_contact(attrs) do
|
def create_contact(attrs) do
|
||||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||||
|
|
||||||
|
|
@ -336,26 +338,63 @@ defmodule Microwaveprop.Radio do
|
||||||
grid1 = Ecto.Changeset.get_change(changeset, :grid1)
|
grid1 = Ecto.Changeset.get_change(changeset, :grid1)
|
||||||
grid2 = Ecto.Changeset.get_change(changeset, :grid2)
|
grid2 = Ecto.Changeset.get_change(changeset, :grid2)
|
||||||
|
|
||||||
case {Maidenhead.to_latlon(grid1), Maidenhead.to_latlon(grid2)} do
|
if duplicate_contact_exists?(changeset) do
|
||||||
{{:ok, {lat1, lon1}}, {:ok, {lat2, lon2}}} ->
|
changeset =
|
||||||
distance = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
|
Ecto.Changeset.add_error(changeset, :station1, "a matching contact already exists within the last hour")
|
||||||
|
|
||||||
changeset
|
{:error, %{changeset | action: :insert}}
|
||||||
|> Ecto.Changeset.put_change(:pos1, %{"lat" => lat1, "lon" => lon1})
|
else
|
||||||
|> Ecto.Changeset.put_change(:pos2, %{"lat" => lat2, "lon" => lon2})
|
case {Maidenhead.to_latlon(grid1), Maidenhead.to_latlon(grid2)} do
|
||||||
|> Ecto.Changeset.put_change(:distance_km, distance)
|
{{:ok, {lat1, lon1}}, {:ok, {lat2, lon2}}} ->
|
||||||
|> Ecto.Changeset.put_change(:user_submitted, true)
|
distance = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
|
||||||
|> Repo.insert()
|
|
||||||
|
|
||||||
_ ->
|
changeset
|
||||||
changeset = Ecto.Changeset.add_error(changeset, :grid1, "could not resolve grid to coordinates")
|
|> Ecto.Changeset.put_change(:pos1, %{"lat" => lat1, "lon" => lon1})
|
||||||
{:error, %{changeset | action: :insert}}
|
|> Ecto.Changeset.put_change(:pos2, %{"lat" => lat2, "lon" => lon2})
|
||||||
|
|> Ecto.Changeset.put_change(:distance_km, distance)
|
||||||
|
|> Ecto.Changeset.put_change(:user_submitted, true)
|
||||||
|
|> Repo.insert()
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
changeset = Ecto.Changeset.add_error(changeset, :grid1, "could not resolve grid to coordinates")
|
||||||
|
{:error, %{changeset | action: :insert}}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:error, %{changeset | action: :insert}}
|
{:error, %{changeset | action: :insert}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp duplicate_contact_exists?(changeset) do
|
||||||
|
s1 = Ecto.Changeset.get_field(changeset, :station1) |> to_string() |> String.upcase()
|
||||||
|
s2 = Ecto.Changeset.get_field(changeset, :station2) |> to_string() |> String.upcase()
|
||||||
|
g1 = Ecto.Changeset.get_field(changeset, :grid1) |> to_string() |> String.upcase()
|
||||||
|
g2 = Ecto.Changeset.get_field(changeset, :grid2) |> to_string() |> String.upcase()
|
||||||
|
band = Ecto.Changeset.get_field(changeset, :band)
|
||||||
|
ts = Ecto.Changeset.get_field(changeset, :qso_timestamp)
|
||||||
|
|
||||||
|
band_decimal = if is_binary(band), do: Decimal.new(band), else: band
|
||||||
|
min_ts = DateTime.add(ts, -@dedup_window_seconds, :second)
|
||||||
|
max_ts = DateTime.add(ts, @dedup_window_seconds, :second)
|
||||||
|
|
||||||
|
# Query for contacts with same band in the time window, then check
|
||||||
|
# station/grid pair match (order-independent) in Elixir
|
||||||
|
from(c in Contact,
|
||||||
|
where:
|
||||||
|
c.band == ^band_decimal and
|
||||||
|
c.qso_timestamp >= ^min_ts and
|
||||||
|
c.qso_timestamp <= ^max_ts and
|
||||||
|
c.flagged_invalid == false
|
||||||
|
)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.any?(fn c ->
|
||||||
|
a = {String.upcase(c.station1), String.upcase(c.grid1)}
|
||||||
|
b = {String.upcase(c.station2), String.upcase(c.grid2)}
|
||||||
|
pair = Enum.sort([a, b])
|
||||||
|
pair == Enum.sort([{s1, g1}, {s2, g2}])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
# ── Contact Edits ───────────────────────────────────────────────
|
# ── Contact Edits ───────────────────────────────────────────────
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -92,4 +92,40 @@ defmodule Microwaveprop.Radio.ContactSubmissionTest do
|
||||||
refute Ecto.Changeset.get_change(changeset, :user_submitted)
|
refute Ecto.Changeset.get_change(changeset, :user_submitted)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "create_contact/1 deduplication" do
|
||||||
|
alias Microwaveprop.Radio
|
||||||
|
|
||||||
|
test "rejects duplicate contact within 1-hour window" do
|
||||||
|
assert {:ok, _contact} = Radio.create_contact(@valid_attrs)
|
||||||
|
|
||||||
|
assert {:error, changeset} = Radio.create_contact(@valid_attrs)
|
||||||
|
assert "a matching contact already exists within the last hour" in errors_on(changeset).station1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows same stations on a different band" do
|
||||||
|
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
||||||
|
assert {:ok, _} = Radio.create_contact(%{@valid_attrs | band: "2304"})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows same stations with different grids" do
|
||||||
|
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
||||||
|
assert {:ok, _} = Radio.create_contact(%{@valid_attrs | grid1: "EM13"})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows contact outside the 1-hour window" do
|
||||||
|
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
||||||
|
|
||||||
|
later = DateTime.add(@valid_attrs.qso_timestamp, 3601, :second)
|
||||||
|
assert {:ok, _} = Radio.create_contact(%{@valid_attrs | qso_timestamp: later})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects duplicates regardless of station order" do
|
||||||
|
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
||||||
|
|
||||||
|
swapped = %{@valid_attrs | station1: "K5TR", grid1: "EM00", station2: "W5XD", grid2: "EM12"}
|
||||||
|
assert {:error, changeset} = Radio.create_contact(swapped)
|
||||||
|
assert "a matching contact already exists within the last hour" in errors_on(changeset).station1
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue