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
|
||||
|
||||
@dedup_window_seconds 3600
|
||||
|
||||
def create_contact(attrs) do
|
||||
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
||||
|
||||
|
|
@ -336,26 +338,63 @@ defmodule Microwaveprop.Radio do
|
|||
grid1 = Ecto.Changeset.get_change(changeset, :grid1)
|
||||
grid2 = Ecto.Changeset.get_change(changeset, :grid2)
|
||||
|
||||
case {Maidenhead.to_latlon(grid1), Maidenhead.to_latlon(grid2)} do
|
||||
{{:ok, {lat1, lon1}}, {:ok, {lat2, lon2}}} ->
|
||||
distance = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
|
||||
if duplicate_contact_exists?(changeset) do
|
||||
changeset =
|
||||
Ecto.Changeset.add_error(changeset, :station1, "a matching contact already exists within the last hour")
|
||||
|
||||
changeset
|
||||
|> Ecto.Changeset.put_change(:pos1, %{"lat" => lat1, "lon" => lon1})
|
||||
|> 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()
|
||||
{:error, %{changeset | action: :insert}}
|
||||
else
|
||||
case {Maidenhead.to_latlon(grid1), Maidenhead.to_latlon(grid2)} do
|
||||
{{:ok, {lat1, lon1}}, {:ok, {lat2, lon2}}} ->
|
||||
distance = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
|
||||
|
||||
_ ->
|
||||
changeset = Ecto.Changeset.add_error(changeset, :grid1, "could not resolve grid to coordinates")
|
||||
{:error, %{changeset | action: :insert}}
|
||||
changeset
|
||||
|> Ecto.Changeset.put_change(:pos1, %{"lat" => lat1, "lon" => lon1})
|
||||
|> 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
|
||||
else
|
||||
{:error, %{changeset | action: :insert}}
|
||||
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 ───────────────────────────────────────────────
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -92,4 +92,40 @@ defmodule Microwaveprop.Radio.ContactSubmissionTest do
|
|||
refute Ecto.Changeset.get_change(changeset, :user_submitted)
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue