defmodule Microwaveprop.Radio.ContactEdit do @moduledoc false use Ecto.Schema import Ecto.Changeset alias Microwaveprop.Accounts.User alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.Maidenhead @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @allowed_modes ~w(CW SSB FM FT8 FT4) @allowed_bands ~w(1296 2304 3456 5760 10000 24000 47000 68000 75000 122000 134000 241000) schema "contact_edits" do belongs_to :contact, Contact belongs_to :user, User belongs_to :reviewed_by, User field :proposed_changes, :map field :status, Ecto.Enum, values: [:pending, :approved, :rejected], default: :pending field :admin_note, :string field :reviewed_at, :utc_datetime timestamps(type: :utc_datetime) end @editable_fields ~w(station1 station2 grid1 grid2 band mode qso_timestamp) def changeset(edit, attrs) do edit |> cast(attrs, [:contact_id, :user_id, :proposed_changes]) |> validate_required([:contact_id, :user_id, :proposed_changes]) |> validate_proposed_changes() |> foreign_key_constraint(:contact_id) |> foreign_key_constraint(:user_id) end def review_changeset(edit, attrs) do edit |> cast(attrs, [:status, :admin_note, :reviewed_by_id, :reviewed_at]) |> validate_required([:status, :reviewed_by_id, :reviewed_at]) |> validate_inclusion(:status, [:approved, :rejected]) end defp validate_proposed_changes(changeset) do case get_field(changeset, :proposed_changes) do nil -> changeset changes when changes == %{} -> add_error(changeset, :proposed_changes, "must contain at least one change") changes when is_map(changes) -> keys = Map.keys(changes) invalid_keys = Enum.reject(keys, &(&1 in @editable_fields)) changeset = if invalid_keys == [] do changeset else add_error(changeset, :proposed_changes, "contains invalid fields: #{Enum.join(invalid_keys, ", ")}") end Enum.reduce(keys, changeset, &validate_field_value/2) _ -> add_error(changeset, :proposed_changes, "must be a map") end end defp validate_field_value("station1", cs), do: validate_callsign_value(cs, "station1") defp validate_field_value("station2", cs), do: validate_callsign_value(cs, "station2") defp validate_field_value("grid1", cs), do: validate_grid_value(cs, "grid1") defp validate_field_value("grid2", cs), do: validate_grid_value(cs, "grid2") defp validate_field_value("band", changeset) do val = get_field(changeset, :proposed_changes)["band"] if to_string(val) in @allowed_bands do changeset else add_error(changeset, :proposed_changes, "band is not a valid frequency") end end defp validate_field_value("mode", changeset) do val = get_field(changeset, :proposed_changes)["mode"] if val in @allowed_modes do changeset else add_error(changeset, :proposed_changes, "mode must be one of: #{Enum.join(@allowed_modes, ", ")}") end end defp validate_field_value("qso_timestamp", changeset) do val = get_field(changeset, :proposed_changes)["qso_timestamp"] case val do %DateTime{} -> changeset s when is_binary(s) -> case DateTime.from_iso8601(s) do {:ok, _, _} -> changeset _ -> add_error(changeset, :proposed_changes, "qso_timestamp is not a valid datetime") end _ -> add_error(changeset, :proposed_changes, "qso_timestamp is not a valid datetime") end end defp validate_field_value(_, changeset), do: changeset defp validate_callsign_value(changeset, field) do val = get_field(changeset, :proposed_changes)[field] || "" val = val |> String.trim() |> String.upcase() if Regex.match?(~r/^[A-Z0-9\/]+$/, val) do changeset else add_error(changeset, :proposed_changes, "#{field} must contain only letters, digits, and /") end end defp validate_grid_value(changeset, field) do val = get_field(changeset, :proposed_changes)[field] || "" val = val |> String.trim() |> String.upcase() if Maidenhead.valid?(val) do changeset else add_error(changeset, :proposed_changes, "#{field} is not a valid Maidenhead grid square") end end end