diff --git a/lib/atlas/accounts.ex b/lib/atlas/accounts.ex index af7d966..b213a62 100644 --- a/lib/atlas/accounts.ex +++ b/lib/atlas/accounts.ex @@ -99,10 +99,17 @@ defmodule Atlas.Accounts do {:error, %Ecto.Changeset{}} """ + def register_user(attrs) do - %User{} - |> User.registration_changeset(attrs) - |> Repo.insert() + Ecto.Multi.new() + |> Ecto.Multi.insert(:user, User.registration_changeset(%User{}, attrs)) + |> create_default_preferences_multi() + |> Repo.transaction() + |> case do + {:ok, %{user: user}} -> {:ok, user} + {:error, :user, changeset, _} -> {:error, changeset} + {:error, :preferences, changeset, _} -> {:error, changeset} + end end @doc """ @@ -539,64 +546,105 @@ defmodule Atlas.Accounts do Repo.delete(user_session) end - ## User Preference + ## User Preferences + + @doc """ + Creates a user preference. + """ + def create_preference(attrs) when is_map(attrs) and map_size(attrs) > 0 do + %UserPreference{} + |> UserPreference.changeset(attrs) + |> Repo.insert() + end + + def create_preference(_), do: {:error, :invalid_fields} + + @doc """ + Updates a user preference. + """ + def update_preference(%UserPreference{} = preference, attrs) + when is_map(attrs) and map_size(attrs) > 0 do + preference + |> UserPreference.changeset(attrs) + |> Repo.update() + end + + def update_preference(_, _), do: {:error, :invalid_fields} @doc """ - Gets the language preference for a given user. + Gets the set of preferences of a given user. ## Examples - iex> get_user_preference(1) + iex> get_user_preferences(1) %UserPreference{} - iex> get_user_preference(999) + iex> get_user_preferences(999) nil """ - def get_user_preference(user_id) do + def get_user_preferences(user_id) do Repo.get_by(UserPreference, user_id: user_id) end @doc """ - Gets the language string for a given user. - - Returns `nil` if no preference is set. + Gets a given preference from the user preferences. ## Examples - iex> get_user_language(1) - "pt-PT" + iex> get_user_preference(1, "language") + "en-US" - iex> get_user_language(999) + iex> get_user_preference(1, "void") nil """ - def get_user_language(user_id) do - Repo.one( - from up in UserPreference, - where: up.user_id == ^user_id, - select: up.language, - limit: 1 - ) + def get_user_preference(user_id, preference) do + preferences = get_user_preferences(user_id) + Map.get(preferences, String.to_atom(preference), nil) end @doc """ - Sets the language preference for a given user. - - If the preference exists, it updates only the language field. + Sets a given preference or preferences for an user. ## Examples - iex> set_user_language(1, "pt-PT") - {:ok, %UserPreference{}} + iex> set_user_preference(%{"user_id" => "1", "language" => "en-US"}) + %UserPreference{} - iex> set_user_language(1, "invalid") - {:error, %Ecto.Changeset{}} + iex> set_user_preference(%{"user_id" => "1", "language" => "pt-PT", "invalid_field" => "none"}) + %UserPreference{} + + iex> set_user_preference(%{"user_id" => "2", "language" => nil}) + {:error, :invalid_fields} + + iex> set_user_preference(%{"user_id" => "2", %{}}) + {:error, :invalid_fields} """ - def set_user_language(user_id, language) do - %UserPreference{} - |> UserPreference.changeset(%{user_id: user_id, language: language}) - |> Repo.insert( - on_conflict: [set: [language: language]], - conflict_target: :user_id - ) + + def set_user_preference(%{"user_id" => user_id} = attrs) when is_binary(user_id) do + ap = get_available_preferences() + + update_fields = + attrs + |> Enum.reject(fn {k, v} -> is_nil(v) or k not in ap end) + |> Enum.map(fn {k, v} -> {String.to_atom(k), v} end) + |> Enum.into(%{}) + + case get_user_preferences(user_id) do + nil -> create_preference(update_fields) + %UserPreference{} = up -> update_preference(up, update_fields) + end + end + + @doc """ + Gets the available user preferences. + """ + def get_available_preferences, do: ["language"] + + @doc false + defp create_default_preferences_multi(multi) do + Ecto.Multi.insert(multi, :preferences, fn %{user: user} -> + %UserPreference{user_id: user.id} + |> UserPreference.changeset(%{}) + end) end end diff --git a/lib/atlas/accounts/user_preferences.ex b/lib/atlas/accounts/user_preferences.ex index 2d9616e..6c1ca66 100644 --- a/lib/atlas/accounts/user_preferences.ex +++ b/lib/atlas/accounts/user_preferences.ex @@ -3,28 +3,24 @@ defmodule Atlas.Accounts.UserPreference do Schema for storing a user's preference. """ - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:id, :binary_id, autogenerate: true} - @foreign_key_type :binary_id + use Atlas.Schema @languages ~w(pt-PT en-US) + @optional_fields ~w(language)a + @required_fields ~w(user_id)a schema "user_preferences" do field :language, :string - belongs_to :user, Atlas.Accounts.User + belongs_to :user, Atlas.Accounts.User, type: :binary_id timestamps() end def changeset(user_preference, attrs) do user_preference - |> cast(attrs, [:user_id, :language]) - |> validate_required([:user_id, :language]) + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) |> validate_inclusion(:language, @languages) - |> assoc_constraint(:user) - |> unique_constraint(:user_id) end end diff --git a/lib/atlas_web/controllers/preferences_controller.ex b/lib/atlas_web/controllers/preferences_controller.ex new file mode 100644 index 0000000..c593b0f --- /dev/null +++ b/lib/atlas_web/controllers/preferences_controller.ex @@ -0,0 +1,45 @@ +defmodule AtlasWeb.PreferencesController do + use AtlasWeb, :controller + + alias Atlas.Accounts + + def get_preferences(conn, _params) do + {user, _session} = Guardian.Plug.current_resource(conn) + preferences = Accounts.get_user_preferences(user.id) + + json(conn, %{ + user_id: preferences.user_id, + language: preferences.language + }) + end + + def get_preference(conn, %{"preference" => preference}) do + {user, _session} = Guardian.Plug.current_resource(conn) + preference_value = Accounts.get_user_preference(user.id, preference) + + case preference_value do + nil -> json(conn, %{error: "Preference not found"}) + _ -> json(conn, %{preference => preference_value}) + end + end + + def update_preferences(conn, attrs) do + {user, _session} = Guardian.Plug.current_resource(conn) + + case Accounts.set_user_preference(Map.put(attrs, "user_id", user.id)) do + {:ok, _} -> + json(conn, %{status: "success", message: "Preferences updated successfully"}) + + {:error, :invalid_fields} -> + json(conn, %{status: "error", message: "No valid fields provided"}) + + {:error, _changeset} -> + json(conn, %{status: "error", message: "No valid values provided"}) + end + end + + def get_available_preferences(conn, _params) do + preferences = Accounts.get_available_preferences() + json(conn, %{preferences: preferences}) + end +end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index 241e834..12aea28 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -44,6 +44,10 @@ defmodule AtlasWeb.Router do pipe_through :auth scope "/auth" do + get "/preferences", PreferencesController, :get_preferences + get "/preferences/:preference", PreferencesController, :get_preference + get "/available_preferences", PreferencesController, :get_available_preferences + put "/preferences", PreferencesController, :update_preferences post "/sign_out", AuthController, :sign_out get "/me", AuthController, :me get "/sessions", AuthController, :sessions diff --git a/priv/repo/migrations/20250726211654_create_user_preferences.exs b/priv/repo/migrations/20250726211654_create_user_preferences.exs index ff7a4c4..96679bb 100644 --- a/priv/repo/migrations/20250726211654_create_user_preferences.exs +++ b/priv/repo/migrations/20250726211654_create_user_preferences.exs @@ -2,14 +2,15 @@ defmodule Atlas.Repo.Migrations.CreateUserPreferences do use Ecto.Migration def change do - create table(:user_preferences) do - add :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false - add :language, :string, null: false + create table(:user_preferences, primary_key: false) do + add :id, :binary_id, primary_key: true + add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false + add :language, :string - timestamps() + timestamps(type: :utc_datetime) end - create unique_index(:user_preferences, [:user_id]) + create index(:user_preferences, [:user_id]) create constraint(:user_preferences, :language_must_be_valid, check: "language IN ('pt-PT', 'en-US')" diff --git a/test/atlas/preferences_test.exs b/test/atlas/preferences_test.exs new file mode 100644 index 0000000..5f19fe6 --- /dev/null +++ b/test/atlas/preferences_test.exs @@ -0,0 +1,92 @@ +defmodule Atlas.PreferencesTest do + use AtlasWeb.ConnCase + + alias AtlasWeb.PreferencesController + + setup do + conn = authenticated_conn(%{type: :student}) + {user, _session} = Guardian.Plug.current_resource(conn) + + %{ + authenticated_conn: conn, + user: user + } + end + + describe "get_preferences/2" do + test "returns the user's preferences", %{authenticated_conn: conn, user: user} do + conn = PreferencesController.get_preferences(conn, %{}) + + response = json_response(conn, 200) + assert response["user_id"] == user.id + assert Map.has_key?(response, "language") + end + end + + describe "get_preference/2" do + test "returns the specific preference (language)", %{authenticated_conn: conn} do + PreferencesController.update_preferences(conn, %{ + "language" => "en-US" + }) + + conn = PreferencesController.get_preference(conn, %{"preference" => "language"}) + + response = json_response(conn, 200) + assert Map.has_key?(response, "language") + assert response["language"] == "en-US" + end + + test "returns error for non-existent preference", %{authenticated_conn: conn} do + conn = PreferencesController.get_preference(conn, %{"preference" => "none"}) + + response = json_response(conn, 200) + assert response["error"] == "Preference not found" + end + end + + describe "update_preference/2" do + test "updates the specific preference", %{authenticated_conn: conn} do + conn = + PreferencesController.update_preferences(conn, %{ + "language" => "pt-PT" + }) + + response = json_response(conn, 200) + assert response["status"] == "success" + assert response["message"] == "Preferences updated successfully" + end + + test "returns error for invalid fields", %{authenticated_conn: conn} do + conn = + PreferencesController.update_preferences(conn, %{ + "invalid_field" => "value" + }) + + response = json_response(conn, 200) + assert response["status"] == "error" + assert response["message"] == "No valid fields provided" + end + + test "returns error for invalid values", %{authenticated_conn: conn} do + conn = + PreferencesController.update_preferences(conn, %{ + "language" => "invalid_value" + }) + + response = json_response(conn, 200) + assert response["status"] == "error" + assert response["message"] == "No valid values provided" + end + end + + describe "get_available_preferences/2" do + test "returns available preferences", %{authenticated_conn: conn} do + conn = PreferencesController.get_available_preferences(conn, %{}) + + response = json_response(conn, 200) + assert Map.has_key?(response, "preferences") + assert is_list(response["preferences"]) + assert "language" in response["preferences"] + end + end +end