Contacts now carry a user_id FK to users. When a logged-in user submits a contact (single or CSV upload), the server injects their user_id and the email from their account into the submission params, and the visible email input is replaced with a hidden input holding that value — logged-in users don't have to retype their address. The submission changeset now requires at least one of user_id or submitter_email (rather than hard-requiring email), and email format is only validated when one is actually present. Reworded the submit-page blurb from "10 GHz to 241 GHz" to "902 MHz and up" so it matches the actual band coverage of the project.
134 lines
4.3 KiB
Elixir
134 lines
4.3 KiB
Elixir
defmodule Microwaveprop.Radio.Contact do
|
|
@moduledoc false
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
alias Microwaveprop.Radio.Maidenhead
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
@foreign_key_type :binary_id
|
|
|
|
schema "contacts" do
|
|
field :station1, :string
|
|
field :station2, :string
|
|
field :qso_timestamp, :utc_datetime
|
|
field :grid1, :string
|
|
field :grid2, :string
|
|
field :pos1, :map
|
|
field :pos2, :map
|
|
field :mode, :string
|
|
field :band, :decimal
|
|
field :distance_km, :decimal
|
|
|
|
field :hrrr_status, Ecto.Enum,
|
|
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
|
default: :pending
|
|
|
|
field :weather_status, Ecto.Enum,
|
|
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
|
default: :pending
|
|
|
|
field :terrain_status, Ecto.Enum,
|
|
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
|
default: :pending
|
|
|
|
field :iemre_status, Ecto.Enum,
|
|
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
|
default: :pending
|
|
|
|
field :user_submitted, :boolean, default: false
|
|
field :submitter_email, :string
|
|
field :user_id, :binary_id
|
|
field :flagged_invalid, :boolean, default: false
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@required_fields ~w(station1 station2 qso_timestamp mode band)a
|
|
@optional_fields ~w(grid1 grid2 pos1 pos2 distance_km user_id)a
|
|
|
|
def changeset(contact, attrs) do
|
|
contact
|
|
|> cast(attrs, @required_fields ++ @optional_fields)
|
|
|> validate_required(@required_fields)
|
|
end
|
|
|
|
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_id)a
|
|
@submission_required ~w(station1 station2 qso_timestamp mode band grid1 grid2)a
|
|
@allowed_modes ~w(CW SSB FM FT8 FT4)
|
|
@allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 68000 75000 122000 134000 241000), &Decimal.new/1)
|
|
|
|
def submission_changeset(contact, attrs) do
|
|
contact
|
|
|> cast(attrs, @submission_fields)
|
|
|> validate_required(@submission_required)
|
|
|> validate_user_or_email()
|
|
|> sanitize_callsign(:station1)
|
|
|> sanitize_callsign(:station2)
|
|
|> sanitize_grid(:grid1)
|
|
|> sanitize_grid(:grid2)
|
|
|> validate_callsign(:station1)
|
|
|> validate_callsign(:station2)
|
|
|> validate_grid_format(:grid1)
|
|
|> validate_grid_format(:grid2)
|
|
|> validate_inclusion(:mode, @allowed_modes)
|
|
|> validate_inclusion(:band, @allowed_bands)
|
|
|> validate_email_format()
|
|
|> validate_length(:submitter_email, max: 254)
|
|
|> validate_length(:station1, max: 20)
|
|
|> validate_length(:station2, max: 20)
|
|
end
|
|
|
|
# Either a user_id (logged-in submitter) or a submitter_email (anonymous)
|
|
# must be present so we know who submitted the contact.
|
|
defp validate_user_or_email(changeset) do
|
|
user_id = get_field(changeset, :user_id)
|
|
email = get_field(changeset, :submitter_email)
|
|
|
|
if user_id || (email && email != "") do
|
|
changeset
|
|
else
|
|
add_error(changeset, :submitter_email, "can't be blank")
|
|
end
|
|
end
|
|
|
|
defp validate_email_format(changeset) do
|
|
case get_field(changeset, :submitter_email) do
|
|
nil -> changeset
|
|
"" -> changeset
|
|
_email -> validate_format(changeset, :submitter_email, ~r/^[^\s<>]+@[^\s<>]+\.[^\s<>]+$/)
|
|
end
|
|
end
|
|
|
|
# Strip whitespace and upcase callsigns
|
|
defp sanitize_callsign(changeset, field) do
|
|
case get_change(changeset, field) do
|
|
nil -> changeset
|
|
val -> put_change(changeset, field, val |> String.trim() |> String.upcase())
|
|
end
|
|
end
|
|
|
|
# Upcase grid squares (Maidenhead uses uppercase letters + digits)
|
|
defp sanitize_grid(changeset, field) do
|
|
case get_change(changeset, field) do
|
|
nil -> changeset
|
|
val -> put_change(changeset, field, val |> String.trim() |> String.upcase())
|
|
end
|
|
end
|
|
|
|
# Callsigns: letters, digits, and / only (e.g. W5XD, KG5CCI/P, VE3/W5XD)
|
|
defp validate_callsign(changeset, field) do
|
|
validate_format(changeset, field, ~r/^[A-Z0-9\/]+$/, message: "must contain only letters, digits, and /")
|
|
end
|
|
|
|
defp validate_grid_format(changeset, field) do
|
|
validate_change(changeset, field, fn _, value ->
|
|
if Maidenhead.valid?(value) do
|
|
[]
|
|
else
|
|
[{field, "is not a valid Maidenhead grid square"}]
|
|
end
|
|
end)
|
|
end
|
|
end
|