Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 83 additions & 35 deletions lib/atlas/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down Expand Up @@ -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
16 changes: 6 additions & 10 deletions lib/atlas/accounts/user_preferences.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 45 additions & 0 deletions lib/atlas_web/controllers/preferences_controller.ex
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lib/atlas_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions priv/repo/migrations/20250726211654_create_user_preferences.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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')"
Expand Down
92 changes: 92 additions & 0 deletions test/atlas/preferences_test.exs
Original file line number Diff line number Diff line change
@@ -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