From 38920e9f3dc8d4ae971d99ad2dee45d68e48304b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 10 Jul 2025 16:07:05 -0500 Subject: [PATCH] add callsign to users --- lib/aprsme/accounts.ex | 40 ++++ lib/aprsme/accounts/user.ex | 43 +++- lib/aprsme_web/live/user_registration_live.ex | 22 +++ lib/aprsme_web/live/user_settings_live.ex | 171 +++++++++++++++- .../20250710205402_add_callsign_to_users.exs | 11 ++ test/aprsme/accounts/user_test.exs | 96 +++++++++ .../live/user_registration_live_test.exs | 3 +- .../live/user_settings_live_test.exs | 183 ++++++++++++++++++ test/support/fixtures/accounts_fixtures.ex | 8 +- 9 files changed, 567 insertions(+), 10 deletions(-) create mode 100644 priv/repo/migrations/20250710205402_add_callsign_to_users.exs create mode 100644 test/aprsme/accounts/user_test.exs create mode 100644 test/aprsme_web/live/user_settings_live_test.exs diff --git a/lib/aprsme/accounts.ex b/lib/aprsme/accounts.ex index dd34fe6..6868627 100644 --- a/lib/aprsme/accounts.ex +++ b/lib/aprsme/accounts.ex @@ -181,6 +181,46 @@ defmodule Aprsme.Accounts do UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token)) end + @doc """ + Returns an `%Ecto.Changeset{}` for changing the user callsign. + + ## Examples + + iex> change_user_callsign(user) + %Ecto.Changeset{data: %User{}} + + """ + def change_user_callsign(user, attrs \\ %{}) do + User.callsign_changeset(user, attrs, validate_callsign: false) + end + + @doc """ + Updates the user callsign with password validation. + + ## Examples + + iex> update_user_callsign(user, "valid password", %{callsign: "K1ABC"}) + {:ok, %User{}} + + iex> update_user_callsign(user, "invalid password", %{callsign: "K1ABC"}) + {:error, %Ecto.Changeset{}} + + """ + def update_user_callsign(user, password, attrs) do + changeset = + user + |> User.callsign_changeset(attrs) + |> User.validate_current_password(password) + + Ecto.Multi.new() + |> Ecto.Multi.update(:user, changeset) + |> Repo.transaction() + |> case do + {:ok, %{user: user}} -> {:ok, user} + {:error, :user, changeset, _} -> {:error, changeset} + end + end + @doc """ Returns an `%Ecto.Changeset{}` for changing the user password. diff --git a/lib/aprsme/accounts/user.ex b/lib/aprsme/accounts/user.ex index a311716..b6e16fe 100644 --- a/lib/aprsme/accounts/user.ex +++ b/lib/aprsme/accounts/user.ex @@ -6,6 +6,7 @@ defmodule Aprsme.Accounts.User do schema "users" do field(:email, :string) + field(:callsign, :string) field(:password, :string, virtual: true, redact: true) field(:hashed_password, :string, redact: true) field(:confirmed_at, :naive_datetime) @@ -38,8 +39,9 @@ defmodule Aprsme.Accounts.User do """ def registration_changeset(user, attrs, opts \\ []) do user - |> cast(attrs, [:email, :password]) + |> cast(attrs, [:email, :password, :callsign]) |> validate_email(opts) + |> validate_callsign(opts) |> validate_password(opts) end @@ -51,6 +53,17 @@ defmodule Aprsme.Accounts.User do |> maybe_validate_unique_email(opts) end + defp validate_callsign(changeset, opts) do + changeset + |> validate_required([:callsign]) + |> validate_format(:callsign, ~r/^[A-Z]{1,2}[0-9]{1,2}[A-Z]{1,3}$/i, + message: "must be a valid amateur radio callsign" + ) + |> update_change(:callsign, &String.upcase/1) + |> validate_length(:callsign, min: 3, max: 10) + |> maybe_validate_unique_callsign(opts) + end + defp validate_password(changeset, opts) do changeset |> validate_required([:password]) @@ -95,6 +108,19 @@ defmodule Aprsme.Accounts.User do defp do_validate_unique_email(changeset, false), do: changeset + defp maybe_validate_unique_callsign(changeset, opts) do + validate_callsign? = Keyword.get(opts, :validate_callsign, true) + do_validate_unique_callsign(changeset, validate_callsign?) + end + + defp do_validate_unique_callsign(changeset, true) do + changeset + |> unsafe_validate_unique(:callsign, Aprsme.Repo) + |> unique_constraint(:callsign) + end + + defp do_validate_unique_callsign(changeset, false), do: changeset + @doc """ A user changeset for changing the email. @@ -110,6 +136,21 @@ defmodule Aprsme.Accounts.User do end end + @doc """ + A user changeset for changing the callsign. + + It requires the callsign to change otherwise an error is added. + """ + def callsign_changeset(user, attrs, opts \\ []) do + user + |> cast(attrs, [:callsign]) + |> validate_callsign(opts) + |> case do + %{changes: %{callsign: _}} = changeset -> changeset + %{} = changeset -> add_error(changeset, :callsign, "did not change") + end + end + @doc """ A user changeset for changing the password. diff --git a/lib/aprsme_web/live/user_registration_live.ex b/lib/aprsme_web/live/user_registration_live.ex index a1accf8..5c618a2 100644 --- a/lib/aprsme_web/live/user_registration_live.ex +++ b/lib/aprsme_web/live/user_registration_live.ex @@ -60,6 +60,28 @@ defmodule AprsmeWeb.UserRegistrationLive do +
+ + + +
+