diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a100790..52a6d629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - Code quality improvements - Fix dead link of example bullet abbreviations - Fix inaccurate error message when updating shot records +- Fix tables not sorting dates correctly +- Fix container table not displaying all fields # v0.8.3 - Improve some styles diff --git a/lib/cannery/accounts.ex b/lib/cannery/accounts.ex index 0adb6d2a..6cf6efc6 100644 --- a/lib/cannery/accounts.ex +++ b/lib/cannery/accounts.ex @@ -385,8 +385,18 @@ defmodule Cannery.Accounts do """ @spec allow_registration?() :: boolean() def allow_registration? do - Application.get_env(:cannery, Cannery.Accounts)[:registration] == "public" or - list_users_by_role(:admin) |> Enum.empty?() + registration_mode() == :public or list_users_by_role(:admin) |> Enum.empty?() + end + + @doc """ + Returns an atom representing the current configured registration mode + """ + @spec registration_mode() :: :public | :invite_only + def registration_mode do + case Application.get_env(:cannery, Cannery.Accounts)[:registration] do + "public" -> :public + _other -> :invite_only + end end @doc """ diff --git a/lib/cannery/accounts/invites.ex b/lib/cannery/accounts/invites.ex index 7f8e955d..93c0f303 100644 --- a/lib/cannery/accounts/invites.ex +++ b/lib/cannery/accounts/invites.ex @@ -100,13 +100,23 @@ defmodule Cannery.Accounts.Invites do end end - @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer() - def get_use_count(%Invite{id: invite_id}, %User{role: :admin}) do - Repo.one( + @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer() | nil + def get_use_count(%Invite{id: invite_id} = invite, user) do + [invite] |> get_use_counts(user) |> Map.get(invite_id) + end + + @spec get_use_counts([Invite.t()], User.t()) :: + %{optional(Invite.id()) => non_neg_integer()} + def get_use_counts(invites, %User{role: :admin}) do + invite_ids = invites |> Enum.map(fn %{id: invite_id} -> invite_id end) + + Repo.all( from u in User, - where: u.invite_id == ^invite_id, - select: count(u.id) + where: u.invite_id in ^invite_ids, + group_by: u.invite_id, + select: {u.invite_id, count(u.id)} ) + |> Map.new() end @spec decrement_invite_changeset(Invite.t()) :: Invite.changeset() diff --git a/lib/cannery/activity_log.ex b/lib/cannery/activity_log.ex index 3d9aa35e..d24bbf03 100644 --- a/lib/cannery/activity_log.ex +++ b/lib/cannery/activity_log.ex @@ -4,7 +4,8 @@ defmodule Cannery.ActivityLog do """ import Ecto.Query, warn: false - alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo} + alias Cannery.Ammo.{AmmoGroup, AmmoType} + alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Repo} alias Ecto.Multi @doc """ @@ -31,8 +32,10 @@ defmodule Cannery.ActivityLog do Repo.all( from sg in ShotGroup, - left_join: ag in assoc(sg, :ammo_group), - left_join: at in assoc(ag, :ammo_type), + left_join: ag in AmmoGroup, + on: sg.ammo_group_id == ag.id, + left_join: at in AmmoType, + on: ag.ammo_type_id == at.id, where: sg.user_id == ^user_id, where: fragment( @@ -61,6 +64,18 @@ defmodule Cannery.ActivityLog do ) end + @spec list_shot_groups_for_ammo_group(AmmoGroup.t(), User.t()) :: [ShotGroup.t()] + def list_shot_groups_for_ammo_group( + %AmmoGroup{id: ammo_group_id, user_id: user_id}, + %User{id: user_id} + ) do + Repo.all( + from sg in ShotGroup, + where: sg.ammo_group_id == ^ammo_group_id, + where: sg.user_id == ^user_id + ) + end + @doc """ Gets a single shot_group. @@ -107,9 +122,15 @@ defmodule Cannery.ActivityLog do ) |> Multi.run( :ammo_group, - fn repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} -> - {:ok, - repo.one(from ag in AmmoGroup, where: ag.id == ^ammo_group_id and ag.user_id == ^user_id)} + fn _repo, %{create_shot_group: %{ammo_group_id: ammo_group_id, user_id: user_id}} -> + ammo_group = + Repo.one( + from ag in AmmoGroup, + where: ag.id == ^ammo_group_id, + where: ag.user_id == ^user_id + ) + + {:ok, ammo_group} end ) |> Multi.update( @@ -220,4 +241,112 @@ defmodule Cannery.ActivityLog do {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} end end + + @doc """ + Returns the number of shot rounds for an ammo group + """ + @spec get_used_count(AmmoGroup.t(), User.t()) :: non_neg_integer() + def get_used_count(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do + [ammo_group] + |> get_used_counts(user) + |> Map.get(ammo_group_id, 0) + end + + @doc """ + Returns the number of shot rounds for multiple ammo groups + """ + @spec get_used_counts([AmmoGroup.t()], User.t()) :: + %{optional(AmmoGroup.id()) => non_neg_integer()} + def get_used_counts(ammo_groups, %User{id: user_id}) do + ammo_group_ids = + ammo_groups + |> Enum.map(fn %{id: ammo_group_id} -> ammo_group_id end) + + Repo.all( + from sg in ShotGroup, + where: sg.ammo_group_id in ^ammo_group_ids, + where: sg.user_id == ^user_id, + group_by: sg.ammo_group_id, + select: {sg.ammo_group_id, sum(sg.count)} + ) + |> Map.new() + end + + @doc """ + Returns the last entered shot group date for an ammo group + """ + @spec get_last_used_date(AmmoGroup.t(), User.t()) :: Date.t() | nil + def get_last_used_date(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do + [ammo_group] + |> get_last_used_dates(user) + |> Map.get(ammo_group_id) + end + + @doc """ + Returns the last entered shot group date for an ammo group + """ + @spec get_last_used_dates([AmmoGroup.t()], User.t()) :: %{optional(AmmoGroup.id()) => Date.t()} + def get_last_used_dates(ammo_groups, %User{id: user_id}) do + ammo_group_ids = + ammo_groups + |> Enum.map(fn %AmmoGroup{id: ammo_group_id, user_id: ^user_id} -> ammo_group_id end) + + Repo.all( + from sg in ShotGroup, + where: sg.ammo_group_id in ^ammo_group_ids, + where: sg.user_id == ^user_id, + group_by: sg.ammo_group_id, + select: {sg.ammo_group_id, max(sg.date)} + ) + |> Map.new() + end + + @doc """ + Gets the total number of rounds shot for an ammo type + + Raises `Ecto.NoResultsError` if the Ammo type does not exist. + + ## Examples + + iex> get_used_count_for_ammo_type(123, %User{id: 123}) + 35 + + iex> get_used_count_for_ammo_type(456, %User{id: 123}) + ** (Ecto.NoResultsError) + + """ + @spec get_used_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer() + def get_used_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do + [ammo_type] + |> get_used_count_for_ammo_types(user) + |> Map.get(ammo_type_id, 0) + end + + @doc """ + Gets the total number of rounds shot for multiple ammo types + + ## Examples + + iex> get_used_count_for_ammo_types(123, %User{id: 123}) + 35 + + """ + @spec get_used_count_for_ammo_types([AmmoType.t()], User.t()) :: + %{optional(AmmoType.id()) => non_neg_integer()} + def get_used_count_for_ammo_types(ammo_types, %User{id: user_id}) do + ammo_type_ids = + ammo_types + |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end) + + Repo.all( + from ag in AmmoGroup, + left_join: sg in ShotGroup, + on: ag.id == sg.ammo_group_id, + where: ag.ammo_type_id in ^ammo_type_ids, + where: not (sg.count |> is_nil()), + group_by: ag.ammo_type_id, + select: {ag.ammo_type_id, sum(sg.count)} + ) + |> Map.new() + end end diff --git a/lib/cannery/activity_log/shot_group.ex b/lib/cannery/activity_log/shot_group.ex index f4a66ad5..32d7e2c8 100644 --- a/lib/cannery/activity_log/shot_group.ex +++ b/lib/cannery/activity_log/shot_group.ex @@ -6,7 +6,7 @@ defmodule Cannery.ActivityLog.ShotGroup do use Ecto.Schema import CanneryWeb.Gettext import Ecto.Changeset - alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo} + alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup} alias Ecto.{Changeset, UUID} @derive {Jason.Encoder, @@ -24,25 +24,23 @@ defmodule Cannery.ActivityLog.ShotGroup do field :date, :date field :notes, :string - belongs_to :user, User - belongs_to :ammo_group, AmmoGroup + field :user_id, :binary_id + field :ammo_group_id, :binary_id timestamps() end - @type t :: %ShotGroup{ + @type t :: %__MODULE__{ id: id(), count: integer, notes: String.t() | nil, date: Date.t() | nil, - ammo_group: AmmoGroup.t() | nil, ammo_group_id: AmmoGroup.id(), - user: User.t() | nil, user_id: User.id(), inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } - @type new_shot_group :: %ShotGroup{} + @type new_shot_group :: %__MODULE__{} @type id :: UUID.t() @type changeset :: Changeset.t(t() | new_shot_group()) @@ -58,44 +56,47 @@ defmodule Cannery.ActivityLog.ShotGroup do %User{id: user_id}, %AmmoGroup{id: ammo_group_id, user_id: user_id} = ammo_group, attrs - ) - when not (user_id |> is_nil()) and not (ammo_group_id |> is_nil()) do + ) do shot_group |> change(user_id: user_id) |> change(ammo_group_id: ammo_group_id) |> cast(attrs, [:count, :notes, :date]) - |> validate_number(:count, greater_than: 0) |> validate_create_shot_group_count(ammo_group) - |> validate_required([:count, :date, :ammo_group_id, :user_id]) + |> validate_required([:date, :ammo_group_id, :user_id]) end def create_changeset(shot_group, _invalid_user, _invalid_ammo_group, attrs) do shot_group |> cast(attrs, [:count, :notes, :date]) - |> validate_number(:count, greater_than: 0) - |> validate_required([:count, :ammo_group_id, :user_id]) + |> validate_required([:ammo_group_id, :user_id]) |> add_error(:invalid, dgettext("errors", "Please select a valid user and ammo pack")) end defp validate_create_shot_group_count(changeset, %AmmoGroup{count: ammo_group_count}) do - if changeset |> Changeset.get_field(:count) > ammo_group_count do - error = - dgettext("errors", "Count must be less than %{count} shots", count: ammo_group_count) + case changeset |> Changeset.get_field(:count) do + nil -> + changeset |> Changeset.add_error(:ammo_left, dgettext("errors", "can't be blank")) - changeset |> Changeset.add_error(:count, error) - else - changeset + count when count > ammo_group_count -> + changeset + |> Changeset.add_error(:ammo_left, dgettext("errors", "Ammo left must be at least 0")) + + count when count <= 0 -> + error = + dgettext("errors", "Ammo left can be at most %{count} rounds", + count: ammo_group_count - 1 + ) + + changeset |> Changeset.add_error(:ammo_left, error) + + _valid_count -> + changeset end end @doc false @spec update_changeset(t() | new_shot_group(), User.t(), attrs :: map()) :: changeset() - def update_changeset( - %ShotGroup{user_id: user_id} = shot_group, - %User{id: user_id} = user, - attrs - ) - when not (user_id |> is_nil()) do + def update_changeset(%__MODULE__{} = shot_group, user, attrs) do shot_group |> cast(attrs, [:count, :notes, :date]) |> validate_number(:count, greater_than: 0) @@ -105,12 +106,10 @@ defmodule Cannery.ActivityLog.ShotGroup do defp validate_update_shot_group_count( changeset, - %ShotGroup{count: count} = shot_group, - %User{id: user_id} - ) - when not (user_id |> is_nil()) do - %{ammo_group: %AmmoGroup{count: ammo_group_count, user_id: ^user_id}} = - shot_group |> Repo.preload(:ammo_group) + %__MODULE__{ammo_group_id: ammo_group_id, count: count}, + user + ) do + %{count: ammo_group_count} = Ammo.get_ammo_group!(ammo_group_id, user) new_shot_group_count = changeset |> Changeset.get_field(:count) shot_diff_to_add = new_shot_group_count - count diff --git a/lib/cannery/ammo.ex b/lib/cannery/ammo.ex index ece49a80..3760e8c7 100644 --- a/lib/cannery/ammo.ex +++ b/lib/cannery/ammo.ex @@ -5,12 +5,15 @@ defmodule Cannery.Ammo do import CanneryWeb.Gettext import Ecto.Query, warn: false - alias Cannery.{Accounts.User, Containers, Containers.Container, Repo} - alias Cannery.ActivityLog.ShotGroup + alias Cannery.{Accounts.User, Containers, Repo} + alias Cannery.Containers.{Container, ContainerTag, Tag} + alias Cannery.{ActivityLog, ActivityLog.ShotGroup} alias Cannery.Ammo.{AmmoGroup, AmmoType} alias Ecto.Changeset @ammo_group_create_limit 10_000 + @ammo_group_preloads [:ammo_type] + @ammo_type_preloads [:ammo_groups] @doc """ Returns the list of ammo_types. @@ -28,8 +31,14 @@ defmodule Cannery.Ammo do @spec list_ammo_types(search :: nil | String.t(), User.t()) :: [AmmoType.t()] def list_ammo_types(search \\ nil, user) - def list_ammo_types(search, %{id: user_id}) when search |> is_nil() or search == "", - do: Repo.all(from at in AmmoType, where: at.user_id == ^user_id, order_by: at.name) + def list_ammo_types(search, %{id: user_id}) when search |> is_nil() or search == "" do + Repo.all( + from at in AmmoType, + where: at.user_id == ^user_id, + order_by: at.name, + preload: ^@ammo_type_preloads + ) + end def list_ammo_types(search, %{id: user_id}) when search |> is_binary() do trimmed_search = String.trim(search) @@ -39,16 +48,19 @@ defmodule Cannery.Ammo do where: at.user_id == ^user_id, where: fragment( - "search @@ websearch_to_tsquery('english', ?)", + "? @@ websearch_to_tsquery('english', ?)", + at.search, ^trimmed_search ), order_by: { :desc, fragment( - "ts_rank_cd(search, websearch_to_tsquery('english', ?), 4)", + "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", + at.search, ^trimmed_search ) - } + }, + preload: ^@ammo_type_preloads ) end @@ -86,114 +98,169 @@ defmodule Cannery.Ammo do """ @spec get_ammo_type!(AmmoType.id(), User.t()) :: AmmoType.t() - def get_ammo_type!(id, %User{id: user_id}), - do: Repo.one!(from at in AmmoType, where: at.id == ^id and at.user_id == ^user_id) + def get_ammo_type!(id, %User{id: user_id}) do + Repo.one!( + from at in AmmoType, + where: at.id == ^id, + where: at.user_id == ^user_id, + preload: ^@ammo_type_preloads + ) + end @doc """ - Gets the average cost of a single ammo type + Gets the average cost of an ammo type from ammo groups with price information. ## Examples - iex> get_average_cost_for_ammo_type!(%AmmoType{id: 123}, %User{id: 123}) + iex> get_average_cost_for_ammo_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) 1.50 """ - @spec get_average_cost_for_ammo_type!(AmmoType.t(), User.t()) :: float() | nil - def get_average_cost_for_ammo_type!( - %AmmoType{id: ammo_type_id, user_id: user_id}, - %User{id: user_id} - ) do + @spec get_average_cost_for_ammo_type(AmmoType.t(), User.t()) :: float() | nil + def get_average_cost_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do + [ammo_type] + |> get_average_cost_for_ammo_types(user) + |> Map.get(ammo_type_id) + end + + @doc """ + Gets the average cost of ammo types from ammo groups with price information + for multiple ammo types. + + ## Examples + + iex> get_average_cost_for_ammo_types( + ...> [%AmmoType{id: 123, user_id: 456}], + ...> %User{id: 456} + ...> ) + 1.50 + + """ + @spec get_average_cost_for_ammo_types([AmmoType.t()], User.t()) :: + %{optional(AmmoType.id()) => float()} + def get_average_cost_for_ammo_types(ammo_types, %User{id: user_id}) do + ammo_type_ids = + ammo_types + |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end) + sg_total_query = from sg in ShotGroup, where: not (sg.count |> is_nil()), group_by: sg.ammo_group_id, select: %{ammo_group_id: sg.ammo_group_id, total: sum(sg.count)} - Repo.one!( + Repo.all( from ag in AmmoGroup, as: :ammo_group, left_join: sg_query in subquery(sg_total_query), on: ag.id == sg_query.ammo_group_id, - where: ag.ammo_type_id == ^ammo_type_id, + where: ag.ammo_type_id in ^ammo_type_ids, + group_by: ag.ammo_type_id, where: not (ag.price_paid |> is_nil()), - select: sum(ag.price_paid) / sum(ag.count + coalesce(sg_query.total, 0)) + select: + {ag.ammo_type_id, sum(ag.price_paid) / sum(ag.count + coalesce(sg_query.total, 0))} ) + |> Map.new() end @doc """ Gets the total number of rounds for an ammo type - Raises `Ecto.NoResultsError` if the Ammo type does not exist. - ## Examples - iex> get_round_count_for_ammo_type(123, %User{id: 123}) + iex> get_round_count_for_ammo_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) 35 - iex> get_round_count_for_ammo_type(456, %User{id: 123}) - ** (Ecto.NoResultsError) - """ @spec get_round_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer() - def get_round_count_for_ammo_type( - %AmmoType{id: ammo_type_id, user_id: user_id}, - %User{id: user_id} - ) do - Repo.one!( - from ag in AmmoGroup, - where: ag.ammo_type_id == ^ammo_type_id, - select: sum(ag.count) - ) || 0 + def get_round_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do + [ammo_type] + |> get_round_count_for_ammo_types(user) + |> Map.get(ammo_type_id, 0) end @doc """ - Gets the total number of rounds shot for an ammo type - - Raises `Ecto.NoResultsError` if the Ammo type does not exist. + Gets the total number of rounds for multiple ammo types ## Examples - iex> get_used_count_for_ammo_type(123, %User{id: 123}) - 35 - - iex> get_used_count_for_ammo_type(456, %User{id: 123}) - ** (Ecto.NoResultsError) + iex> get_round_count_for_ammo_types( + ...> [%AmmoType{id: 123, user_id: 456}], + ...> %User{id: 456} + ...> ) + %{123 => 35} """ - @spec get_used_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer() - def get_used_count_for_ammo_type( - %AmmoType{id: ammo_type_id, user_id: user_id}, - %User{id: user_id} - ) do - Repo.one!( + @spec get_round_count_for_ammo_types([AmmoType.t()], User.t()) :: + %{optional(AmmoType.id()) => non_neg_integer()} + def get_round_count_for_ammo_types(ammo_types, %User{id: user_id}) do + ammo_type_ids = + ammo_types + |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end) + + Repo.all( from ag in AmmoGroup, - left_join: sg in assoc(ag, :shot_groups), - where: ag.ammo_type_id == ^ammo_type_id, - select: sum(sg.count) - ) || 0 + where: ag.ammo_type_id in ^ammo_type_ids, + where: ag.user_id == ^user_id, + group_by: ag.ammo_type_id, + select: {ag.ammo_type_id, sum(ag.count)} + ) + |> Map.new() end @doc """ Gets the total number of ammo ever bought for an ammo type - Raises `Ecto.NoResultsError` if the Ammo type does not exist. - ## Examples - iex> get_historical_count_for_ammo_type(123, %User{id: 123}) - %AmmoType{} - - iex> get_historical_count_for_ammo_type(456, %User{id: 123}) - ** (Ecto.NoResultsError) + iex> get_historical_count_for_ammo_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) + 5 """ @spec get_historical_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer() - def get_historical_count_for_ammo_type( - %AmmoType{user_id: user_id} = ammo_type, - %User{id: user_id} = user - ) do - get_round_count_for_ammo_type(ammo_type, user) + - get_used_count_for_ammo_type(ammo_type, user) + def get_historical_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do + [ammo_type] + |> get_historical_count_for_ammo_types(user) + |> Map.get(ammo_type_id, 0) + end + + @doc """ + Gets the total number of ammo ever bought for multiple ammo types + + ## Examples + + iex> get_historical_count_for_ammo_types( + ...> [%AmmoType{id: 123, user_id: 456}], + ...> %User{id: 456} + ...> ) + %{123 => 5} + + """ + @spec get_historical_count_for_ammo_types([AmmoType.t()], User.t()) :: + %{optional(AmmoType.id()) => non_neg_integer()} + def get_historical_count_for_ammo_types(ammo_types, %User{id: user_id} = user) do + used_counts = ammo_types |> ActivityLog.get_used_count_for_ammo_types(user) + round_counts = ammo_types |> get_round_count_for_ammo_types(user) + + ammo_types + |> Enum.filter(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> + Map.has_key?(used_counts, ammo_type_id) or Map.has_key?(round_counts, ammo_type_id) + end) + |> Map.new(fn %{id: ammo_type_id} -> + historical_count = + Map.get(used_counts, ammo_type_id, 0) + Map.get(round_counts, ammo_type_id, 0) + + {ammo_type_id, historical_count} + end) end @doc """ @@ -210,8 +277,21 @@ defmodule Cannery.Ammo do """ @spec create_ammo_type(attrs :: map(), User.t()) :: {:ok, AmmoType.t()} | {:error, AmmoType.changeset()} - def create_ammo_type(attrs \\ %{}, %User{} = user), - do: %AmmoType{} |> AmmoType.create_changeset(user, attrs) |> Repo.insert() + def create_ammo_type(attrs \\ %{}, %User{} = user) do + %AmmoType{} + |> AmmoType.create_changeset(user, attrs) + |> Repo.insert() + |> case do + {:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()} + {:error, changeset} -> {:error, changeset} + end + end + + @spec preload_ammo_type(AmmoType.t()) :: AmmoType.t() + @spec preload_ammo_type([AmmoType.t()]) :: [AmmoType.t()] + defp preload_ammo_type(ammo_type_or_ammo_types) do + ammo_type_or_ammo_types |> Repo.preload(@ammo_type_preloads) + end @doc """ Updates a ammo_type. @@ -227,8 +307,15 @@ defmodule Cannery.Ammo do """ @spec update_ammo_type(AmmoType.t(), attrs :: map(), User.t()) :: {:ok, AmmoType.t()} | {:error, AmmoType.changeset()} - def update_ammo_type(%AmmoType{user_id: user_id} = ammo_type, attrs, %User{id: user_id}), - do: ammo_type |> AmmoType.update_changeset(attrs) |> Repo.update() + def update_ammo_type(%AmmoType{user_id: user_id} = ammo_type, attrs, %User{id: user_id}) do + ammo_type + |> AmmoType.update_changeset(attrs) + |> Repo.update() + |> case do + {:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()} + {:error, changeset} -> {:error, changeset} + end + end @doc """ Deletes a ammo_type. @@ -244,30 +331,48 @@ defmodule Cannery.Ammo do """ @spec delete_ammo_type(AmmoType.t(), User.t()) :: {:ok, AmmoType.t()} | {:error, AmmoType.changeset()} - def delete_ammo_type(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}), - do: ammo_type |> Repo.delete() + def delete_ammo_type(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}) do + ammo_type + |> Repo.delete() + |> case do + {:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()} + {:error, changeset} -> {:error, changeset} + end + end @doc """ Deletes a ammo_type. ## Examples - iex> delete_ammo_type(ammo_type, %User{id: 123}) + iex> delete_ammo_type!(ammo_type, %User{id: 123}) %AmmoType{} """ @spec delete_ammo_type!(AmmoType.t(), User.t()) :: AmmoType.t() - def delete_ammo_type!(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}), - do: ammo_type |> Repo.delete!() + def delete_ammo_type!(ammo_type, user) do + {:ok, ammo_type} = delete_ammo_type(ammo_type, user) + ammo_type + end @doc """ Returns the list of ammo_groups for a user and type. ## Examples - iex> list_ammo_groups_for_type(%AmmoType{id: 123}, %User{id: 123}) + iex> list_ammo_groups_for_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) [%AmmoGroup{}, ...] + iex> list_ammo_groups_for_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456}, + ...> true + ...> ) + [%AmmoGroup{}, %AmmoGroup{}, ...] + """ @spec list_ammo_groups_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()] @spec list_ammo_groups_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) :: @@ -281,11 +386,9 @@ defmodule Cannery.Ammo do ) do Repo.all( from ag in AmmoGroup, - left_join: sg in assoc(ag, :shot_groups), where: ag.ammo_type_id == ^ammo_type_id, where: ag.user_id == ^user_id, - preload: [shot_groups: sg], - order_by: ag.id + preload: ^@ammo_group_preloads ) end @@ -296,12 +399,10 @@ defmodule Cannery.Ammo do ) do Repo.all( from ag in AmmoGroup, - left_join: sg in assoc(ag, :shot_groups), where: ag.ammo_type_id == ^ammo_type_id, where: ag.user_id == ^user_id, where: not (ag.count == 0), - preload: [shot_groups: sg], - order_by: ag.id + preload: ^@ammo_group_preloads ) end @@ -310,9 +411,19 @@ defmodule Cannery.Ammo do ## Examples - iex> list_ammo_groups_for_container(%AmmoType{id: 123}, %User{id: 123}) + iex> list_ammo_groups_for_container( + ...> %Container{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) [%AmmoGroup{}, ...] + iex> list_ammo_groups_for_container( + ...> %Container{id: 123, user_id: 456}, + ...> %User{id: 456}, + ...> true + ...> ) + [%AmmoGroup{}, %AmmoGroup{}, ...] + """ @spec list_ammo_groups_for_container(Container.t(), User.t()) :: [AmmoGroup.t()] @spec list_ammo_groups_for_container(Container.t(), User.t(), include_empty :: boolean()) :: @@ -326,11 +437,9 @@ defmodule Cannery.Ammo do ) do Repo.all( from ag in AmmoGroup, - left_join: sg in assoc(ag, :shot_groups), where: ag.container_id == ^container_id, where: ag.user_id == ^user_id, - preload: [shot_groups: sg], - order_by: ag.id + preload: ^@ammo_group_preloads ) end @@ -341,12 +450,10 @@ defmodule Cannery.Ammo do ) do Repo.all( from ag in AmmoGroup, - left_join: sg in assoc(ag, :shot_groups), where: ag.container_id == ^container_id, where: ag.user_id == ^user_id, where: not (ag.count == 0), - preload: [shot_groups: sg], - order_by: ag.id + preload: ^@ammo_group_preloads ) end @@ -355,45 +462,76 @@ defmodule Cannery.Ammo do ## Examples - iex> get_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123}) + iex> get_ammo_groups_count_for_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) 3 - iex> get_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123}, true) + iex> get_ammo_groups_count_for_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456}, + ...> true + ...> ) 5 """ - @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()] + @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer() @spec get_ammo_groups_count_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) :: - [AmmoGroup.t()] - def get_ammo_groups_count_for_type(ammo_type, user, include_empty \\ false) - + non_neg_integer() def get_ammo_groups_count_for_type( - %AmmoType{id: ammo_type_id, user_id: user_id}, - %User{id: user_id}, - true = _include_empty + %AmmoType{id: ammo_type_id} = ammo_type, + user, + include_empty \\ false ) do - Repo.one!( - from ag in AmmoGroup, - where: ag.user_id == ^user_id, - where: ag.ammo_type_id == ^ammo_type_id, - distinct: true, - select: count(ag.id) - ) || 0 + [ammo_type] + |> get_ammo_groups_count_for_types(user, include_empty) + |> Map.get(ammo_type_id, 0) end - def get_ammo_groups_count_for_type( - %AmmoType{id: ammo_type_id, user_id: user_id}, - %User{id: user_id}, - false = _include_empty - ) do - Repo.one!( - from ag in AmmoGroup, - where: ag.user_id == ^user_id, - where: ag.ammo_type_id == ^ammo_type_id, - where: not (ag.count == 0), - distinct: true, - select: count(ag.id) - ) || 0 + @doc """ + Returns the count of ammo_groups for multiple ammo types. + + ## Examples + + iex> get_ammo_groups_count_for_types( + ...> [%AmmoType{id: 123, user_id: 456}], + ...> %User{id: 456} + ...> ) + 3 + + iex> get_ammo_groups_count_for_types( + ...> [%AmmoType{id: 123, user_id: 456}], + ...> %User{id: 456}, + ...> true + ...> ) + 5 + + """ + @spec get_ammo_groups_count_for_types([AmmoType.t()], User.t()) :: + %{optional(AmmoType.id()) => non_neg_integer()} + @spec get_ammo_groups_count_for_types([AmmoType.t()], User.t(), include_empty :: boolean()) :: + %{optional(AmmoType.id()) => non_neg_integer()} + def get_ammo_groups_count_for_types(ammo_types, %User{id: user_id}, include_empty \\ false) do + ammo_type_ids = + ammo_types + |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end) + + from(ag in AmmoGroup, + where: ag.user_id == ^user_id, + where: ag.ammo_type_id in ^ammo_type_ids, + group_by: ag.ammo_type_id, + select: {ag.ammo_type_id, count(ag.id)} + ) + |> maybe_include_empty(include_empty) + |> Repo.all() + |> Map.new() + end + + defp maybe_include_empty(query, true), do: query + + defp maybe_include_empty(query, _false) do + query |> where([ag], not (ag.count == 0)) end @doc """ @@ -401,23 +539,147 @@ defmodule Cannery.Ammo do ## Examples - iex> get_used_ammo_groups_count_for_type(%AmmoType{id: 123}, %User{id: 123}) + iex> get_used_ammo_groups_count_for_type( + ...> %AmmoType{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) 3 """ - @spec get_used_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()] - def get_used_ammo_groups_count_for_type( - %AmmoType{id: ammo_type_id, user_id: user_id}, - %User{id: user_id} - ) do - Repo.one!( + @spec get_used_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer() + def get_used_ammo_groups_count_for_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do + [ammo_type] + |> get_used_ammo_groups_count_for_types(user) + |> Map.get(ammo_type_id, 0) + end + + @doc """ + Returns the count of used ammo_groups for multiple ammo types. + + ## Examples + + iex> get_used_ammo_groups_count_for_types( + ...> [%AmmoType{id: 123, user_id: 456}], + ...> %User{id: 456} + ...> ) + %{123 => 3} + + """ + @spec get_used_ammo_groups_count_for_types([AmmoType.t()], User.t()) :: + %{optional(AmmoType.id()) => non_neg_integer()} + def get_used_ammo_groups_count_for_types(ammo_types, %User{id: user_id}) do + ammo_type_ids = + ammo_types + |> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end) + + Repo.all( from ag in AmmoGroup, where: ag.user_id == ^user_id, - where: ag.ammo_type_id == ^ammo_type_id, + where: ag.ammo_type_id in ^ammo_type_ids, where: ag.count == 0, - distinct: true, - select: count(ag.id) - ) || 0 + group_by: ag.ammo_type_id, + select: {ag.ammo_type_id, count(ag.id)} + ) + |> Map.new() + end + + @doc """ + Returns number of ammo packs in a container. + + ## Examples + + iex> get_ammo_groups_count_for_container( + ...> %Container{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) + 3 + + """ + @spec get_ammo_groups_count_for_container!(Container.t(), User.t()) :: non_neg_integer() + def get_ammo_groups_count_for_container!( + %Container{id: container_id} = container, + %User{} = user + ) do + [container] + |> get_ammo_groups_count_for_containers(user) + |> Map.get(container_id, 0) + end + + @doc """ + Returns number of ammo packs in multiple containers. + + ## Examples + + iex> get_ammo_groups_count_for_containers( + ...> [%Container{id: 123, user_id: 456}], + ...> %User{id: 456} + ...> ) + %{123 => 3} + + """ + @spec get_ammo_groups_count_for_containers([Container.t()], User.t()) :: %{ + Container.id() => non_neg_integer() + } + def get_ammo_groups_count_for_containers(containers, %User{id: user_id}) do + container_ids = + containers + |> Enum.map(fn %Container{id: container_id, user_id: ^user_id} -> container_id end) + + Repo.all( + from ag in AmmoGroup, + where: ag.container_id in ^container_ids, + where: ag.count != 0, + group_by: ag.container_id, + select: {ag.container_id, count(ag.id)} + ) + |> Map.new() + end + + @doc """ + Returns number of rounds in a container. + + ## Examples + + iex> get_round_count_for_container( + ...> %Container{id: 123, user_id: 456}, + ...> %User{id: 456} + ...> ) + 5 + + """ + @spec get_round_count_for_container!(Container.t(), User.t()) :: non_neg_integer() + def get_round_count_for_container!(%Container{id: container_id} = container, user) do + [container] + |> get_round_count_for_containers(user) + |> Map.get(container_id, 0) + end + + @doc """ + Returns number of ammo packs in multiple containers. + + ## Examples + + iex> get_round_count_for_containers( + ...> [%Container{id: 123, user_id: 456}], + ...> %User{id: 456} + ...> ) + %{123 => 5} + + """ + @spec get_round_count_for_containers([Container.t()], User.t()) :: + %{Container.id() => non_neg_integer()} + def get_round_count_for_containers(containers, %User{id: user_id}) do + container_ids = + containers + |> Enum.map(fn %Container{id: container_id, user_id: ^user_id} -> container_id end) + + Repo.all( + from ag in AmmoGroup, + where: ag.container_id in ^container_ids, + group_by: ag.container_id, + select: {ag.container_id, sum(ag.count)} + ) + |> Map.new() end @doc """ @@ -440,17 +702,20 @@ defmodule Cannery.Ammo do from( ag in AmmoGroup, as: :ag, - left_join: sg in assoc(ag, :shot_groups), - as: :sg, join: at in assoc(ag, :ammo_type), as: :at, - join: c in assoc(ag, :container), + join: c in Container, + on: ag.container_id == c.id, + on: ag.user_id == c.user_id, as: :c, - left_join: t in assoc(c, :tags), + left_join: ct in ContainerTag, + on: c.id == ct.container_id, + left_join: t in Tag, + on: ct.tag_id == t.id, + on: c.user_id == t.user_id, as: :t, where: ag.user_id == ^user_id, - preload: [shot_groups: sg, ammo_type: at, container: {c, tags: t}], - order_by: ag.id + preload: ^@ammo_group_preloads ) |> list_ammo_groups_include_empty(include_empty) |> list_ammo_groups_search(search) @@ -517,18 +782,16 @@ defmodule Cannery.Ammo do def list_staged_ammo_groups(%User{id: user_id}) do Repo.all( from ag in AmmoGroup, - left_join: sg in assoc(ag, :shot_groups), where: ag.user_id == ^user_id, where: ag.staged == true, - preload: [shot_groups: sg], - order_by: ag.id + preload: ^@ammo_group_preloads ) end @doc """ Gets a single ammo_group. - Raises `Ecto.NoResultsError` if the Ammo group does not exist. + Raises `KeyError` if the Ammo group does not exist. ## Examples @@ -536,76 +799,142 @@ defmodule Cannery.Ammo do %AmmoGroup{} iex> get_ammo_group!(456, %User{id: 123}) - ** (Ecto.NoResultsError) + ** (KeyError) """ @spec get_ammo_group!(AmmoGroup.id(), User.t()) :: AmmoGroup.t() - def get_ammo_group!(id, %User{id: user_id}) do - Repo.one!( + def get_ammo_group!(id, user) do + [id] |> get_ammo_groups(user) |> Map.fetch!(id) + end + + @doc """ + Gets a group of ammo_groups by their ID. + + ## Examples + + iex> get_ammo_groups([123, 456], %User{id: 123}) + %{123 => %AmmoGroup{}, 456 => %AmmoGroup{}} + + """ + @spec get_ammo_groups([AmmoGroup.id()], User.t()) :: + %{optional(AmmoGroup.id()) => AmmoGroup.t()} + def get_ammo_groups(ids, %User{id: user_id}) do + Repo.all( from ag in AmmoGroup, - left_join: sg in assoc(ag, :shot_groups), - where: ag.id == ^id, + where: ag.id in ^ids, where: ag.user_id == ^user_id, - preload: [shot_groups: sg] + preload: ^@ammo_group_preloads, + select: {ag.id, ag} ) - end - - @doc """ - Returns the number of shot rounds for an ammo group - """ - @spec get_used_count(AmmoGroup.t()) :: non_neg_integer() - def get_used_count(%AmmoGroup{} = ammo_group) do - ammo_group - |> Repo.preload(:shot_groups) - |> Map.fetch!(:shot_groups) - |> Enum.map(fn %{count: count} -> count end) - |> Enum.sum() - end - - @doc """ - Returns the last entered shot group for an ammo group - """ - @spec get_last_used_shot_group(AmmoGroup.t()) :: ShotGroup.t() | nil - def get_last_used_shot_group(%AmmoGroup{} = ammo_group) do - ammo_group - |> Repo.preload(:shot_groups) - |> Map.fetch!(:shot_groups) - |> Enum.max_by(fn %{date: date} -> date end, Date, fn -> nil end) + |> Map.new() end @doc """ Calculates the percentage remaining of an ammo group out of 100 + + ## Examples + + iex> get_percentage_remaining( + ...> %AmmoGroup{id: 123, count: 5, user_id: 456}, + ...> %User{id: 456} + ...> ) + 100 + """ - @spec get_percentage_remaining(AmmoGroup.t()) :: non_neg_integer() - def get_percentage_remaining(%AmmoGroup{count: 0}), do: 0 + @spec get_percentage_remaining(AmmoGroup.t(), User.t()) :: non_neg_integer() + def get_percentage_remaining(%AmmoGroup{count: 0, user_id: user_id}, %User{id: user_id}) do + 0 + end - def get_percentage_remaining(%AmmoGroup{count: count} = ammo_group) do - ammo_group = ammo_group |> Repo.preload(:shot_groups) - - shot_group_sum = - ammo_group.shot_groups - |> Enum.map(fn %{count: count} -> count end) - |> Enum.sum() - - round(count / (count + shot_group_sum) * 100) + def get_percentage_remaining(%AmmoGroup{count: count} = ammo_group, current_user) do + round(count / get_original_count(ammo_group, current_user) * 100) end @doc """ Gets the original count for an ammo group + + ## Examples + + iex> get_original_count( + ...> %AmmoGroup{id: 123, count: 5, user_id: 456}, + ...> %User{id: 456} + ...> ) + 5 + """ - @spec get_original_count(AmmoGroup.t()) :: non_neg_integer() - def get_original_count(%AmmoGroup{count: count} = ammo_group) do - count + get_used_count(ammo_group) + @spec get_original_count(AmmoGroup.t(), User.t()) :: non_neg_integer() + def get_original_count(%AmmoGroup{id: ammo_group_id} = ammo_group, current_user) do + [ammo_group] + |> get_original_counts(current_user) + |> Map.fetch!(ammo_group_id) + end + + @doc """ + Gets the original counts for multiple ammo groups + + ## Examples + + iex> get_original_counts( + ...> [%AmmoGroup{id: 123, count: 5, user_id: 456}], + ...> %User{id: 456} + ...> ) + %{123 => 5} + + """ + @spec get_original_counts([AmmoGroup.t()], User.t()) :: + %{optional(AmmoGroup.id()) => non_neg_integer()} + def get_original_counts(ammo_groups, %User{id: user_id} = current_user) do + used_counts = ActivityLog.get_used_counts(ammo_groups, current_user) + + ammo_groups + |> Map.new(fn %AmmoGroup{id: ammo_group_id, count: count, user_id: ^user_id} -> + {ammo_group_id, count + Map.get(used_counts, ammo_group_id, 0)} + end) end @doc """ Calculates the CPR for a single ammo group - """ - @spec get_cpr(AmmoGroup.t()) :: nil | float() - def get_cpr(%AmmoGroup{price_paid: nil}), do: nil - def get_cpr(%AmmoGroup{price_paid: price_paid} = ammo_group), - do: calculate_cpr(price_paid, get_original_count(ammo_group)) + ## Examples + + iex> get_cpr( + ...> %AmmoGroup{id: 123, price_paid: 5, count: 5, user_id: 456}, + ...> %User{id: 456} + ...> ) + 1 + + """ + @spec get_cpr(AmmoGroup.t(), User.t()) :: float() | nil + def get_cpr(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do + [ammo_group] + |> get_cprs(user) + |> Map.get(ammo_group_id) + end + + @doc """ + Calculates the CPR for multiple ammo groups + + ## Examples + + iex> get_cprs( + ...> [%AmmoGroup{id: 123, price_paid: 5, count: 5, user_id: 456}], + ...> %User{id: 456} + ...> ) + %{123 => 1} + + """ + @spec get_cprs([AmmoGroup.t()], User.t()) :: %{optional(AmmoGroup.id()) => float()} + def get_cprs(ammo_groups, %User{id: user_id} = current_user) do + original_counts = get_original_counts(ammo_groups, current_user) + + ammo_groups + |> Enum.reject(fn %AmmoGroup{price_paid: price_paid, user_id: ^user_id} -> + price_paid |> is_nil() + end) + |> Map.new(fn %{id: ammo_group_id, price_paid: price_paid} -> + {ammo_group_id, calculate_cpr(price_paid, Map.fetch!(original_counts, ammo_group_id))} + end) + end @spec calculate_cpr(price_paid :: float() | nil, count :: integer()) :: float() | nil defp calculate_cpr(nil, _count), do: nil @@ -660,7 +989,7 @@ defmodule Cannery.Ammo do returning: true ) - {:ok, {count, inserted_ammo_groups}} + {:ok, {count, inserted_ammo_groups |> preload_ammo_group()}} else changesets |> Enum.reject(fn %{valid?: valid} -> valid end) @@ -674,7 +1003,7 @@ defmodule Cannery.Ammo do _multiplier, user ) - when not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do + when is_binary(ammo_type_id) and is_binary(container_id) do changeset = %AmmoGroup{} |> AmmoGroup.create_changeset( @@ -692,6 +1021,12 @@ defmodule Cannery.Ammo do {:error, %AmmoGroup{} |> AmmoGroup.create_changeset(nil, nil, user, invalid_attrs)} end + @spec preload_ammo_group(AmmoGroup.t()) :: AmmoGroup.t() + @spec preload_ammo_group([AmmoGroup.t()]) :: [AmmoGroup.t()] + defp preload_ammo_group(ammo_group_or_ammo_groups) do + ammo_group_or_ammo_groups |> Repo.preload(@ammo_group_preloads) + end + @doc """ Updates a ammo_group. @@ -710,8 +1045,15 @@ defmodule Cannery.Ammo do %AmmoGroup{user_id: user_id} = ammo_group, attrs, %User{id: user_id} = user - ), - do: ammo_group |> AmmoGroup.update_changeset(attrs, user) |> Repo.update() + ) do + ammo_group + |> AmmoGroup.update_changeset(attrs, user) + |> Repo.update() + |> case do + {:ok, ammo_group} -> {:ok, ammo_group |> preload_ammo_group()} + {:error, changeset} -> {:error, changeset} + end + end @doc """ Deletes a ammo_group. @@ -727,8 +1069,14 @@ defmodule Cannery.Ammo do """ @spec delete_ammo_group(AmmoGroup.t(), User.t()) :: {:ok, AmmoGroup.t()} | {:error, AmmoGroup.changeset()} - def delete_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}), - do: ammo_group |> Repo.delete() + def delete_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}) do + ammo_group + |> Repo.delete() + |> case do + {:ok, ammo_group} -> {:ok, ammo_group |> preload_ammo_group()} + {:error, changeset} -> {:error, changeset} + end + end @doc """ Deletes a ammo_group. @@ -740,6 +1088,8 @@ defmodule Cannery.Ammo do """ @spec delete_ammo_group!(AmmoGroup.t(), User.t()) :: AmmoGroup.t() - def delete_ammo_group!(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}), - do: ammo_group |> Repo.delete!() + def delete_ammo_group!(ammo_group, user) do + {:ok, ammo_group} = delete_ammo_group(ammo_group, user) + ammo_group + end end diff --git a/lib/cannery/ammo/ammo_group.ex b/lib/cannery/ammo/ammo_group.ex index f5c7f5c5..676ead86 100644 --- a/lib/cannery/ammo/ammo_group.ex +++ b/lib/cannery/ammo/ammo_group.ex @@ -9,8 +9,8 @@ defmodule Cannery.Ammo.AmmoGroup do use Ecto.Schema import CanneryWeb.Gettext import Ecto.Changeset - alias Cannery.Ammo.{AmmoGroup, AmmoType} - alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Containers, Containers.Container} + alias Cannery.Ammo.AmmoType + alias Cannery.{Accounts.User, Containers, Containers.Container} alias Ecto.{Changeset, UUID} @derive {Jason.Encoder, @@ -33,15 +33,13 @@ defmodule Cannery.Ammo.AmmoGroup do field :purchased_on, :date belongs_to :ammo_type, AmmoType - belongs_to :container, Container - belongs_to :user, User - - has_many :shot_groups, ShotGroup + field :container_id, :binary_id + field :user_id, :binary_id timestamps() end - @type t :: %AmmoGroup{ + @type t :: %__MODULE__{ id: id(), count: integer, notes: String.t() | nil, @@ -50,14 +48,12 @@ defmodule Cannery.Ammo.AmmoGroup do purchased_on: Date.t(), ammo_type: AmmoType.t() | nil, ammo_type_id: AmmoType.id(), - container: Container.t() | nil, container_id: Container.id(), - user: User.t() | nil, user_id: User.id(), inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } - @type new_ammo_group :: %AmmoGroup{} + @type new_ammo_group :: %__MODULE__{} @type id :: UUID.t() @type changeset :: Changeset.t(t() | new_ammo_group()) @@ -76,8 +72,7 @@ defmodule Cannery.Ammo.AmmoGroup do %User{id: user_id}, attrs ) - when not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) and - not (user_id |> is_nil()) do + when is_binary(ammo_type_id) and is_binary(container_id) and is_binary(user_id) do ammo_group |> change(ammo_type_id: ammo_type_id) |> change(user_id: user_id) diff --git a/lib/cannery/ammo/ammo_type.ex b/lib/cannery/ammo/ammo_type.ex index 9a2d7d16..c66b9a77 100644 --- a/lib/cannery/ammo/ammo_type.ex +++ b/lib/cannery/ammo/ammo_type.ex @@ -8,7 +8,7 @@ defmodule Cannery.Ammo.AmmoType do use Ecto.Schema import Ecto.Changeset alias Cannery.Accounts.User - alias Cannery.Ammo.{AmmoGroup, AmmoType} + alias Cannery.Ammo.AmmoGroup alias Ecto.{Changeset, UUID} @derive {Jason.Encoder, @@ -64,14 +64,14 @@ defmodule Cannery.Ammo.AmmoType do field :manufacturer, :string field :upc, :string - belongs_to :user, User + field :user_id, :binary_id has_many :ammo_groups, AmmoGroup timestamps() end - @type t :: %AmmoType{ + @type t :: %__MODULE__{ id: id(), name: String.t(), desc: String.t() | nil, @@ -95,12 +95,11 @@ defmodule Cannery.Ammo.AmmoType do manufacturer: String.t() | nil, upc: String.t() | nil, user_id: User.id(), - user: User.t() | nil, ammo_groups: [AmmoGroup.t()] | nil, inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } - @type new_ammo_type :: %AmmoType{} + @type new_ammo_type :: %__MODULE__{} @type id :: UUID.t() @type changeset :: Changeset.t(t() | new_ammo_type()) diff --git a/lib/cannery/containers.ex b/lib/cannery/containers.ex index 2d718316..20a66dd6 100644 --- a/lib/cannery/containers.ex +++ b/lib/cannery/containers.ex @@ -5,10 +5,12 @@ defmodule Cannery.Containers do import CanneryWeb.Gettext import Ecto.Query, warn: false - alias Cannery.{Accounts.User, Ammo.AmmoGroup, Repo, Tags.Tag} - alias Cannery.Containers.{Container, ContainerTag} + alias Cannery.{Accounts.User, Ammo.AmmoGroup, Repo} + alias Cannery.Containers.{Container, ContainerTag, Tag} alias Ecto.Changeset + @container_preloads [:tags] + @doc """ Returns the list of containers. @@ -28,11 +30,9 @@ defmodule Cannery.Containers do as: :c, left_join: t in assoc(c, :tags), as: :t, - left_join: ag in assoc(c, :ammo_groups), - as: :ag, where: c.user_id == ^user_id, order_by: c.name, - preload: [tags: t, ammo_groups: ag] + preload: ^@container_preloads ) |> list_containers_search(search) |> Repo.all() @@ -106,12 +106,10 @@ defmodule Cannery.Containers do def get_container!(id, %User{id: user_id}) do Repo.one!( from c in Container, - left_join: t in assoc(c, :tags), - left_join: ag in assoc(c, :ammo_groups), where: c.user_id == ^user_id, where: c.id == ^id, order_by: c.name, - preload: [tags: t, ammo_groups: ag] + preload: ^@container_preloads ) end @@ -130,7 +128,19 @@ defmodule Cannery.Containers do @spec create_container(attrs :: map(), User.t()) :: {:ok, Container.t()} | {:error, Container.changeset()} def create_container(attrs, %User{} = user) do - %Container{} |> Container.create_changeset(user, attrs) |> Repo.insert() + %Container{} + |> Container.create_changeset(user, attrs) + |> Repo.insert() + |> case do + {:ok, container} -> {:ok, container |> preload_container()} + {:error, changeset} -> {:error, changeset} + end + end + + @spec preload_container(Container.t()) :: Container.t() + @spec preload_container([Container.t()]) :: [Container.t()] + def preload_container(container) do + container |> Repo.preload(@container_preloads) end @doc """ @@ -148,7 +158,13 @@ defmodule Cannery.Containers do @spec update_container(Container.t(), User.t(), attrs :: map()) :: {:ok, Container.t()} | {:error, Container.changeset()} def update_container(%Container{user_id: user_id} = container, %User{id: user_id}, attrs) do - container |> Container.update_changeset(attrs) |> Repo.update() + container + |> Container.update_changeset(attrs) + |> Repo.update() + |> case do + {:ok, container} -> {:ok, container |> preload_container()} + {:error, changeset} -> {:error, changeset} + end end @doc """ @@ -173,7 +189,12 @@ defmodule Cannery.Containers do ) |> case do 0 -> - container |> Repo.delete() + container + |> Repo.delete() + |> case do + {:ok, container} -> {:ok, container |> preload_container()} + {:error, changeset} -> {:error, changeset} + end _amount -> error = dgettext("errors", "Container must be empty before deleting") @@ -214,8 +235,11 @@ defmodule Cannery.Containers do %Container{user_id: user_id} = container, %Tag{user_id: user_id} = tag, %User{id: user_id} - ), - do: %ContainerTag{} |> ContainerTag.create_changeset(tag, container) |> Repo.insert!() + ) do + %ContainerTag{} + |> ContainerTag.create_changeset(tag, container) + |> Repo.insert!() + end @doc """ Removes a tag from a container @@ -226,45 +250,175 @@ defmodule Cannery.Containers do %Container{} """ - @spec remove_tag!(Container.t(), Tag.t(), User.t()) :: non_neg_integer() + @spec remove_tag!(Container.t(), Tag.t(), User.t()) :: {non_neg_integer(), [ContainerTag.t()]} def remove_tag!( %Container{id: container_id, user_id: user_id}, %Tag{id: tag_id, user_id: user_id}, %User{id: user_id} ) do - {count, _} = + {count, results} = Repo.delete_all( from ct in ContainerTag, where: ct.container_id == ^container_id, - where: ct.tag_id == ^tag_id + where: ct.tag_id == ^tag_id, + select: ct ) - if count == 0, do: raise("could not delete container tag"), else: count + if count == 0, do: raise("could not delete container tag"), else: {count, results} + end + + # Container Tags + + @doc """ + Returns the list of tags. + + ## Examples + + iex> list_tags(%User{id: 123}) + [%Tag{}, ...] + + iex> list_tags("cool", %User{id: 123}) + [%Tag{name: "my cool tag"}, ...] + + """ + @spec list_tags(User.t()) :: [Tag.t()] + @spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()] + def list_tags(search \\ nil, user) + + def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "", + do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name) + + def list_tags(search, %{id: user_id}) when search |> is_binary() do + trimmed_search = String.trim(search) + + Repo.all( + from t in Tag, + where: t.user_id == ^user_id, + where: + fragment( + "? @@ websearch_to_tsquery('english', ?)", + t.search, + ^trimmed_search + ), + order_by: { + :desc, + fragment( + "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", + t.search, + ^trimmed_search + ) + } + ) end @doc """ - Returns number of rounds in container. If data is already preloaded, then - there will be no db hit. + Gets a single tag. + + ## Examples + + iex> get_tag(123, %User{id: 123}) + {:ok, %Tag{}} + + iex> get_tag(456, %User{id: 123}) + {:error, :not_found} + """ - @spec get_container_ammo_group_count!(Container.t()) :: non_neg_integer() - def get_container_ammo_group_count!(%Container{} = container) do - container - |> Repo.preload(:ammo_groups) - |> Map.fetch!(:ammo_groups) - |> Enum.reject(fn %{count: count} -> count == 0 end) - |> Enum.count() + @spec get_tag(Tag.id(), User.t()) :: {:ok, Tag.t()} | {:error, :not_found} + def get_tag(id, %User{id: user_id}) do + Repo.one(from t in Tag, where: t.id == ^id and t.user_id == ^user_id) + |> case do + nil -> {:error, :not_found} + tag -> {:ok, tag} + end end @doc """ - Returns number of rounds in container. If data is already preloaded, then - there will be no db hit. + Gets a single tag. + + Raises `Ecto.NoResultsError` if the Tag does not exist. + + ## Examples + + iex> get_tag!(123, %User{id: 123}) + %Tag{} + + iex> get_tag!(456, %User{id: 123}) + ** (Ecto.NoResultsError) + """ - @spec get_container_rounds!(Container.t()) :: non_neg_integer() - def get_container_rounds!(%Container{} = container) do - container - |> Repo.preload(:ammo_groups) - |> Map.fetch!(:ammo_groups) - |> Enum.map(fn %{count: count} -> count end) - |> Enum.sum() + @spec get_tag!(Tag.id(), User.t()) :: Tag.t() + def get_tag!(id, %User{id: user_id}) do + Repo.one!( + from t in Tag, + where: t.id == ^id, + where: t.user_id == ^user_id + ) + end + + @doc """ + Creates a tag. + + ## Examples + + iex> create_tag(%{field: value}, %User{id: 123}) + {:ok, %Tag{}} + + iex> create_tag(%{field: bad_value}, %User{id: 123}) + {:error, %Changeset{}} + + """ + @spec create_tag(attrs :: map(), User.t()) :: + {:ok, Tag.t()} | {:error, Tag.changeset()} + def create_tag(attrs, %User{} = user) do + %Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert() + end + + @doc """ + Updates a tag. + + ## Examples + + iex> update_tag(tag, %{field: new_value}, %User{id: 123}) + {:ok, %Tag{}} + + iex> update_tag(tag, %{field: bad_value}, %User{id: 123}) + {:error, %Changeset{}} + + """ + @spec update_tag(Tag.t(), attrs :: map(), User.t()) :: + {:ok, Tag.t()} | {:error, Tag.changeset()} + def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}) do + tag |> Tag.update_changeset(attrs) |> Repo.update() + end + + @doc """ + Deletes a tag. + + ## Examples + + iex> delete_tag(tag, %User{id: 123}) + {:ok, %Tag{}} + + iex> delete_tag(tag, %User{id: 123}) + {:error, %Changeset{}} + + """ + @spec delete_tag(Tag.t(), User.t()) :: {:ok, Tag.t()} | {:error, Tag.changeset()} + def delete_tag(%Tag{user_id: user_id} = tag, %User{id: user_id}) do + tag |> Repo.delete() + end + + @doc """ + Deletes a tag. + + ## Examples + + iex> delete_tag!(tag, %User{id: 123}) + %Tag{} + + """ + @spec delete_tag!(Tag.t(), User.t()) :: Tag.t() + def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}) do + tag |> Repo.delete!() end end diff --git a/lib/cannery/containers/container.ex b/lib/cannery/containers/container.ex index cdf8b67f..790fb646 100644 --- a/lib/cannery/containers/container.ex +++ b/lib/cannery/containers/container.ex @@ -6,8 +6,7 @@ defmodule Cannery.Containers.Container do use Ecto.Schema import Ecto.Changeset alias Ecto.{Changeset, UUID} - alias Cannery.Containers.{Container, ContainerTag} - alias Cannery.{Accounts.User, Ammo.AmmoGroup, Tags.Tag} + alias Cannery.{Accounts.User, Containers.ContainerTag, Containers.Tag} @derive {Jason.Encoder, only: [ @@ -26,28 +25,25 @@ defmodule Cannery.Containers.Container do field :location, :string field :type, :string - belongs_to :user, User + field :user_id, :binary_id - has_many :ammo_groups, AmmoGroup many_to_many :tags, Tag, join_through: ContainerTag timestamps() end - @type t :: %Container{ + @type t :: %__MODULE__{ id: id(), name: String.t(), desc: String.t(), location: String.t(), type: String.t(), - user: User.t(), user_id: User.id(), - ammo_groups: [AmmoGroup.t()] | nil, tags: [Tag.t()] | nil, inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } - @type new_container :: %Container{} + @type new_container :: %__MODULE__{} @type id :: UUID.t() @type changeset :: Changeset.t(t() | new_container()) diff --git a/lib/cannery/containers/container_tag.ex b/lib/cannery/containers/container_tag.ex index 4711fc8f..8a61cc09 100644 --- a/lib/cannery/containers/container_tag.ex +++ b/lib/cannery/containers/container_tag.ex @@ -1,12 +1,12 @@ defmodule Cannery.Containers.ContainerTag do @moduledoc """ Thru-table struct for associating Cannery.Containers.Container and - Cannery.Tags.Tag. + Cannery.Containers.Tag. """ use Ecto.Schema import Ecto.Changeset - alias Cannery.{Containers.Container, Containers.ContainerTag, Tags.Tag} + alias Cannery.Containers.{Container, Tag} alias Ecto.{Changeset, UUID} @primary_key {:id, :binary_id, autogenerate: true} @@ -18,7 +18,7 @@ defmodule Cannery.Containers.ContainerTag do timestamps() end - @type t :: %ContainerTag{ + @type t :: %__MODULE__{ id: id(), container: Container.t(), container_id: Container.id(), @@ -27,7 +27,7 @@ defmodule Cannery.Containers.ContainerTag do inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } - @type new_container_tag :: %ContainerTag{} + @type new_container_tag :: %__MODULE__{} @type id :: UUID.t() @type changeset :: Changeset.t(t() | new_container_tag()) diff --git a/lib/cannery/tags/tag.ex b/lib/cannery/containers/tag.ex similarity index 89% rename from lib/cannery/tags/tag.ex rename to lib/cannery/containers/tag.ex index 1dc4dc01..271440ac 100644 --- a/lib/cannery/tags/tag.ex +++ b/lib/cannery/containers/tag.ex @@ -1,4 +1,4 @@ -defmodule Cannery.Tags.Tag do +defmodule Cannery.Containers.Tag do @moduledoc """ Tags are added to containers to help organize, and can include custom-defined text and bg colors. @@ -6,8 +6,8 @@ defmodule Cannery.Tags.Tag do use Ecto.Schema import Ecto.Changeset + alias Cannery.Accounts.User alias Ecto.{Changeset, UUID} - alias Cannery.{Accounts.User, Tags.Tag} @derive {Jason.Encoder, only: [ @@ -23,22 +23,21 @@ defmodule Cannery.Tags.Tag do field :bg_color, :string field :text_color, :string - belongs_to :user, User + field :user_id, :binary_id timestamps() end - @type t :: %Tag{ + @type t :: %__MODULE__{ id: id(), name: String.t(), bg_color: String.t(), text_color: String.t(), - user: User.t() | nil, user_id: User.id(), inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } - @type new_tag() :: %Tag{} + @type new_tag() :: %__MODULE__{} @type id() :: UUID.t() @type changeset() :: Changeset.t(t() | new_tag()) diff --git a/lib/cannery/tags.ex b/lib/cannery/tags.ex deleted file mode 100644 index 8fcda101..00000000 --- a/lib/cannery/tags.ex +++ /dev/null @@ -1,149 +0,0 @@ -defmodule Cannery.Tags do - @moduledoc """ - The Tags context. - """ - - import Ecto.Query, warn: false - import CanneryWeb.Gettext - alias Cannery.{Accounts.User, Repo, Tags.Tag} - - @doc """ - Returns the list of tags. - - ## Examples - - iex> list_tags(%User{id: 123}) - [%Tag{}, ...] - - iex> list_tags("cool", %User{id: 123}) - [%Tag{name: "my cool tag"}, ...] - - """ - @spec list_tags(User.t()) :: [Tag.t()] - @spec list_tags(search :: nil | String.t(), User.t()) :: [Tag.t()] - def list_tags(search \\ nil, user) - - def list_tags(search, %{id: user_id}) when search |> is_nil() or search == "", - do: Repo.all(from t in Tag, where: t.user_id == ^user_id, order_by: t.name) - - def list_tags(search, %{id: user_id}) when search |> is_binary() do - trimmed_search = String.trim(search) - - Repo.all( - from t in Tag, - where: t.user_id == ^user_id, - where: - fragment( - "search @@ websearch_to_tsquery('english', ?)", - ^trimmed_search - ), - order_by: { - :desc, - fragment( - "ts_rank_cd(search, websearch_to_tsquery('english', ?), 4)", - ^trimmed_search - ) - } - ) - end - - @doc """ - Gets a single tag. - - ## Examples - - iex> get_tag(123, %User{id: 123}) - {:ok, %Tag{}} - - iex> get_tag(456, %User{id: 123}) - {:error, "tag not found"} - - """ - @spec get_tag(Tag.id(), User.t()) :: {:ok, Tag.t()} | {:error, String.t()} - def get_tag(id, %User{id: user_id}) do - Repo.one(from t in Tag, where: t.id == ^id and t.user_id == ^user_id) - |> case do - nil -> {:error, dgettext("errors", "Tag not found")} - tag -> {:ok, tag} - end - end - - @doc """ - Gets a single tag. - - Raises `Ecto.NoResultsError` if the Tag does not exist. - - ## Examples - - iex> get_tag!(123, %User{id: 123}) - %Tag{} - - iex> get_tag!(456, %User{id: 123}) - ** (Ecto.NoResultsError) - - """ - @spec get_tag!(Tag.id(), User.t()) :: Tag.t() - def get_tag!(id, %User{id: user_id}), - do: Repo.one!(from t in Tag, where: t.id == ^id and t.user_id == ^user_id) - - @doc """ - Creates a tag. - - ## Examples - - iex> create_tag(%{field: value}, %User{id: 123}) - {:ok, %Tag{}} - - iex> create_tag(%{field: bad_value}, %User{id: 123}) - {:error, %Changeset{}} - - """ - @spec create_tag(attrs :: map(), User.t()) :: - {:ok, Tag.t()} | {:error, Tag.changeset()} - def create_tag(attrs, %User{} = user), - do: %Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert() - - @doc """ - Updates a tag. - - ## Examples - - iex> update_tag(tag, %{field: new_value}, %User{id: 123}) - {:ok, %Tag{}} - - iex> update_tag(tag, %{field: bad_value}, %User{id: 123}) - {:error, %Changeset{}} - - """ - @spec update_tag(Tag.t(), attrs :: map(), User.t()) :: - {:ok, Tag.t()} | {:error, Tag.changeset()} - def update_tag(%Tag{user_id: user_id} = tag, attrs, %User{id: user_id}), - do: tag |> Tag.update_changeset(attrs) |> Repo.update() - - @doc """ - Deletes a tag. - - ## Examples - - iex> delete_tag(tag, %User{id: 123}) - {:ok, %Tag{}} - - iex> delete_tag(tag, %User{id: 123}) - {:error, %Changeset{}} - - """ - @spec delete_tag(Tag.t(), User.t()) :: {:ok, Tag.t()} | {:error, Tag.changeset()} - def delete_tag(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete() - - @doc """ - Deletes a tag. - - ## Examples - - iex> delete_tag!(tag, %User{id: 123}) - %Tag{} - - """ - @spec delete_tag!(Tag.t(), User.t()) :: Tag.t() - def delete_tag!(%Tag{user_id: user_id} = tag, %User{id: user_id}), do: tag |> Repo.delete!() -end diff --git a/lib/cannery_web/components/add_shot_group_component.ex b/lib/cannery_web/components/add_shot_group_component.ex index 47dbae45..cd0b0ebe 100644 --- a/lib/cannery_web/components/add_shot_group_component.ex +++ b/lib/cannery_web/components/add_shot_group_component.ex @@ -5,6 +5,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do use CanneryWeb, :live_component alias Cannery.{Accounts.User, ActivityLog, ActivityLog.ShotGroup, Ammo.AmmoGroup} + alias Ecto.Changeset alias Phoenix.LiveView.{JS, Socket} @impl true @@ -18,7 +19,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do ) :: {:ok, Socket.t()} def update(%{ammo_group: ammo_group, current_user: current_user} = assigns, socket) do changeset = - %ShotGroup{date: NaiveDateTime.utc_now(), count: 1} + %ShotGroup{date: Date.utc_today()} |> ShotGroup.create_changeset(current_user, ammo_group, %{}) {:ok, socket |> assign(assigns) |> assign(:changeset, changeset)} @@ -32,10 +33,13 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do ) do params = shot_group_params |> process_params(ammo_group) + changeset = %ShotGroup{} |> ShotGroup.create_changeset(current_user, ammo_group, params) + changeset = - %ShotGroup{} - |> ShotGroup.create_changeset(current_user, ammo_group, params) - |> Map.put(:action, :validate) + case changeset |> Changeset.apply_action(:validate) do + {:ok, _data} -> changeset + {:error, changeset} -> changeset + end {:noreply, socket |> assign(:changeset, changeset)} end @@ -56,7 +60,7 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do prompt = dgettext("prompts", "Shots recorded successfully") socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) - {:error, %Ecto.Changeset{} = changeset} -> + {:error, %Changeset{} = changeset} -> socket |> assign(changeset: changeset) end @@ -65,14 +69,14 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do # calculate count from shots left defp process_params(params, %AmmoGroup{count: count}) do - new_count = - if params |> Map.get("ammo_left", "0") == "" do - "0" + shot_group_count = + if params |> Map.get("ammo_left", "") == "" do + nil else - params |> Map.get("ammo_left", "0") + new_count = params |> Map.get("ammo_left") |> String.to_integer() + count - new_count end - |> String.to_integer() - params |> Map.put("count", count - new_count) + params |> Map.put("count", shot_group_count) end end diff --git a/lib/cannery_web/components/ammo_group_table_component.ex b/lib/cannery_web/components/ammo_group_table_component.ex index 4bbd7bd2..4c165099 100644 --- a/lib/cannery_web/components/ammo_group_table_component.ex +++ b/lib/cannery_web/components/ammo_group_table_component.ex @@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do A component that displays a list of ammo groups """ use CanneryWeb, :live_component - alias Cannery.{Accounts.User, Ammo, Ammo.AmmoGroup, Repo} + alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.AmmoGroup, Containers} alias Ecto.UUID alias Phoenix.LiveView.{Rendered, Socket} @@ -54,8 +54,8 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do end columns = [ - %{label: gettext("Purchased on"), key: :purchased_on}, - %{label: gettext("Last used on"), key: :used_up_on} | columns + %{label: gettext("Purchased on"), key: :purchased_on, type: Date}, + %{label: gettext("Last used on"), key: :used_up_on, type: Date} | columns ] columns = @@ -94,13 +94,15 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do ammo_type: ammo_type, columns: columns, container: container, + original_counts: Ammo.get_original_counts(ammo_groups, current_user), + cprs: Ammo.get_cprs(ammo_groups, current_user), + last_used_dates: ActivityLog.get_last_used_dates(ammo_groups, current_user), actions: actions, range: range } rows = ammo_groups - |> Repo.preload([:ammo_type, :container]) |> Enum.map(fn ammo_group -> ammo_group |> get_row_data_for_ammo_group(extra_data) end) @@ -124,8 +126,6 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do @spec get_row_data_for_ammo_group(AmmoGroup.t(), additional_data :: map()) :: map() defp get_row_data_for_ammo_group(ammo_group, %{columns: columns} = additional_data) do - ammo_group = ammo_group |> Repo.preload([:ammo_type, :container]) - columns |> Map.new(fn %{key: key} -> {key, get_value_for_key(key, ammo_group, additional_data)} @@ -150,30 +150,23 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do defp get_value_for_key(:price_paid, %{price_paid: nil}, _additional_data), do: {"", nil} defp get_value_for_key(:price_paid, %{price_paid: price_paid}, _additional_data), - do: gettext("$%{amount}", amount: price_paid |> :erlang.float_to_binary(decimals: 2)) - - defp get_value_for_key(:purchased_on, %{purchased_on: purchased_on}, _additional_data) do - assigns = %{purchased_on: purchased_on} + do: gettext("$%{amount}", amount: display_currency(price_paid)) + defp get_value_for_key(:purchased_on, %{purchased_on: purchased_on} = assigns, _additional_data) do {purchased_on, ~H""" - <.date date={@purchased_on} /> + <.date id={"#{@id}-purchased-on"} date={@purchased_on} /> """} end - defp get_value_for_key(:used_up_on, ammo_group, _additional_data) do - last_shot_group_date = - case ammo_group |> Ammo.get_last_used_shot_group() do - %{date: last_shot_group_date} -> last_shot_group_date - _no_shot_groups -> nil - end + defp get_value_for_key(:used_up_on, %{id: ammo_group_id}, %{last_used_dates: last_used_dates}) do + last_used_date = last_used_dates |> Map.get(ammo_group_id) + assigns = %{id: ammo_group_id, last_used_date: last_used_date} - assigns = %{last_shot_group_date: last_shot_group_date} - - {last_shot_group_date, + {last_used_date, ~H""" - <%= if @last_shot_group_date do %> - <.date date={@last_shot_group_date} /> + <%= if @last_used_date do %> + <.date id={"#{@id}-last-used-date"} date={@last_used_date} /> <% else %> <%= gettext("Never used") %> <% end %> @@ -189,8 +182,11 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do """} end - defp get_value_for_key(:remaining, ammo_group, _additional_data), - do: gettext("%{percentage}%", percentage: ammo_group |> Ammo.get_percentage_remaining()) + defp get_value_for_key(:remaining, ammo_group, %{current_user: current_user}), + do: + gettext("%{percentage}%", + percentage: ammo_group |> Ammo.get_percentage_remaining(current_user) + ) defp get_value_for_key(:actions, ammo_group, %{actions: actions}) do assigns = %{actions: actions, ammo_group: ammo_group} @@ -204,31 +200,40 @@ defmodule CanneryWeb.Components.AmmoGroupTableComponent do defp get_value_for_key( :container, - %{container: %{name: container_name}} = ammo_group, - %{container: container} + %{container_id: container_id} = ammo_group, + %{container: container, current_user: current_user} ) do - assigns = %{container: container, ammo_group: ammo_group} + assigns = %{ + container: + %{name: container_name} = container_id |> Containers.get_container!(current_user), + container_block: container, + ammo_group: ammo_group + } {container_name, ~H""" - <%= render_slot(@container, @ammo_group) %> + <%= render_slot(@container_block, {@ammo_group, @container}) %> """} end - defp get_value_for_key(:original_count, ammo_group, _additional_data), - do: ammo_group |> Ammo.get_original_count() + defp get_value_for_key(:original_count, %{id: ammo_group_id}, %{ + original_counts: original_counts + }) do + Map.fetch!(original_counts, ammo_group_id) + end defp get_value_for_key(:cpr, %{price_paid: nil}, _additional_data), do: gettext("No cost information") - defp get_value_for_key(:cpr, ammo_group, _additional_data) do - gettext("$%{amount}", - amount: ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2) - ) + defp get_value_for_key(:cpr, %{id: ammo_group_id}, %{cprs: cprs}) do + gettext("$%{amount}", amount: display_currency(Map.fetch!(cprs, ammo_group_id))) end defp get_value_for_key(:count, %{count: count}, _additional_data), do: if(count == 0, do: gettext("Empty"), else: count) defp get_value_for_key(key, ammo_group, _additional_data), do: ammo_group |> Map.get(key) + + @spec display_currency(float()) :: String.t() + defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) end diff --git a/lib/cannery_web/components/ammo_type_table_component.ex b/lib/cannery_web/components/ammo_type_table_component.ex index ef818f5b..ec36e765 100644 --- a/lib/cannery_web/components/ammo_type_table_component.ex +++ b/lib/cannery_web/components/ammo_type_table_component.ex @@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do A component that displays a list of ammo type """ use CanneryWeb, :live_component - alias Cannery.{Accounts.User, Ammo, Ammo.AmmoType} + alias Cannery.{Accounts.User, ActivityLog, Ammo, Ammo.AmmoType} alias Ecto.UUID alias Phoenix.LiveView.{Rendered, Socket} @@ -103,13 +103,13 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do [ %{ label: gettext("Used packs"), - key: :used_ammo_count, - type: :used_ammo_count + key: :used_packs_count, + type: :used_packs_count }, %{ label: gettext("Total ever packs"), - key: :historical_ammo_count, - type: :historical_ammo_count + key: :historical_packs_count, + type: :historical_packs_count } ] else @@ -121,7 +121,35 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do %{label: nil, key: "actions", type: :actions, sortable: false} ]) - extra_data = %{actions: actions, current_user: current_user} + round_counts = ammo_types |> Ammo.get_round_count_for_ammo_types(current_user) + + used_counts = + show_used && ammo_types |> ActivityLog.get_used_count_for_ammo_types(current_user) + + historical_round_counts = + show_used && ammo_types |> Ammo.get_historical_count_for_ammo_types(current_user) + + packs_count = ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user) + + historical_packs_count = + show_used && ammo_types |> Ammo.get_ammo_groups_count_for_types(current_user, true) + + used_packs_count = + show_used && ammo_types |> Ammo.get_used_ammo_groups_count_for_types(current_user) + + average_costs = ammo_types |> Ammo.get_average_cost_for_ammo_types(current_user) + + extra_data = %{ + actions: actions, + current_user: current_user, + used_counts: used_counts, + round_counts: round_counts, + historical_round_counts: historical_round_counts, + packs_count: packs_count, + used_packs_count: used_packs_count, + historical_packs_count: historical_packs_count, + average_costs: average_costs + } rows = ammo_types @@ -156,28 +184,44 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do defp get_ammo_type_value(:boolean, key, ammo_type, _other_data), do: ammo_type |> Map.get(key) |> humanize() - defp get_ammo_type_value(:round_count, _key, ammo_type, %{current_user: current_user}), - do: ammo_type |> Ammo.get_round_count_for_ammo_type(current_user) + defp get_ammo_type_value(:round_count, _key, %{id: ammo_type_id}, %{round_counts: round_counts}), + do: Map.get(round_counts, ammo_type_id) - defp get_ammo_type_value(:historical_round_count, _key, ammo_type, %{current_user: current_user}), - do: ammo_type |> Ammo.get_historical_count_for_ammo_type(current_user) + defp get_ammo_type_value( + :historical_round_count, + _key, + %{id: ammo_type_id}, + %{historical_round_counts: historical_round_counts} + ), + do: Map.get(historical_round_counts, ammo_type_id) - defp get_ammo_type_value(:used_round_count, _key, ammo_type, %{current_user: current_user}), - do: ammo_type |> Ammo.get_used_count_for_ammo_type(current_user) + defp get_ammo_type_value(:used_round_count, _key, %{id: ammo_type_id}, %{ + used_counts: used_counts + }), + do: Map.get(used_counts, ammo_type_id) - defp get_ammo_type_value(:historical_ammo_count, _key, ammo_type, %{current_user: current_user}), - do: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user, true) + defp get_ammo_type_value( + :historical_packs_count, + _key, + %{id: ammo_type_id}, + %{historical_packs_count: historical_packs_count} + ), + do: Map.get(historical_packs_count, ammo_type_id) - defp get_ammo_type_value(:used_ammo_count, _key, ammo_type, %{current_user: current_user}), - do: ammo_type |> Ammo.get_used_ammo_groups_count_for_type(current_user) + defp get_ammo_type_value(:used_packs_count, _key, %{id: ammo_type_id}, %{ + used_packs_count: used_packs_count + }), + do: Map.get(used_packs_count, ammo_type_id) - defp get_ammo_type_value(:ammo_count, _key, ammo_type, %{current_user: current_user}), - do: ammo_type |> Ammo.get_ammo_groups_count_for_type(current_user) + defp get_ammo_type_value(:ammo_count, _key, %{id: ammo_type_id}, %{packs_count: packs_count}), + do: Map.get(packs_count, ammo_type_id) - defp get_ammo_type_value(:avg_price_paid, _key, ammo_type, %{current_user: current_user}) do - case ammo_type |> Ammo.get_average_cost_for_ammo_type!(current_user) do + defp get_ammo_type_value(:avg_price_paid, _key, %{id: ammo_type_id}, %{ + average_costs: average_costs + }) do + case Map.get(average_costs, ammo_type_id) do nil -> gettext("No cost information") - count -> gettext("$%{amount}", amount: count |> :erlang.float_to_binary(decimals: 2)) + count -> gettext("$%{amount}", amount: display_currency(count)) end end @@ -202,4 +246,7 @@ defmodule CanneryWeb.Components.AmmoTypeTableComponent do defp get_ammo_type_value(nil, _key, _ammo_type, _other_data), do: nil defp get_ammo_type_value(_other, key, ammo_type, _other_data), do: ammo_type |> Map.get(key) + + @spec display_currency(float()) :: String.t() + defp display_currency(float), do: :erlang.float_to_binary(float, decimals: 2) end diff --git a/lib/cannery_web/components/container_table_component.ex b/lib/cannery_web/components/container_table_component.ex index f4393bf8..86379ff4 100644 --- a/lib/cannery_web/components/container_table_component.ex +++ b/lib/cannery_web/components/container_table_component.ex @@ -3,7 +3,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do A component that displays a list of containers """ use CanneryWeb, :live_component - alias Cannery.{Accounts.User, Containers, Containers.Container, Repo} + alias Cannery.{Accounts.User, Ammo, Containers.Container} alias Ecto.UUID alias Phoenix.LiveView.{Rendered, Socket} @@ -45,11 +45,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do %{label: gettext("Name"), key: :name, type: :string}, %{label: gettext("Description"), key: :desc, type: :string}, %{label: gettext("Location"), key: :location, type: :string}, - %{label: gettext("Type"), key: :type, type: :string}, - %{label: gettext("Packs"), key: :packs, type: :integer}, - %{label: gettext("Rounds"), key: :rounds, type: :string}, - %{label: gettext("Tags"), key: :tags, type: :tags}, - %{label: nil, key: :actions, sortable: false, type: :actions} + %{label: gettext("Type"), key: :type, type: :string} ] |> Enum.filter(fn %{key: key, type: type} -> # remove columns if all values match defaults @@ -64,11 +60,19 @@ defmodule CanneryWeb.Components.ContainerTableComponent do type in [:tags, :actions] or not (container |> Map.get(key) == default_value) end) end) + |> Enum.concat([ + %{label: gettext("Packs"), key: :packs, type: :integer}, + %{label: gettext("Rounds"), key: :rounds, type: :integer}, + %{label: gettext("Tags"), key: :tags, type: :tags}, + %{label: nil, key: :actions, sortable: false, type: :actions} + ]) extra_data = %{ current_user: current_user, tag_actions: tag_actions, - actions: actions + actions: actions, + pack_count: Ammo.get_ammo_groups_count_for_containers(containers, current_user), + round_count: Ammo.get_round_count_for_containers(containers, current_user) } rows = @@ -100,8 +104,6 @@ defmodule CanneryWeb.Components.ContainerTableComponent do @spec get_row_data_for_container(Container.t(), columns :: [map()], extra_data :: map) :: map() defp get_row_data_for_container(container, columns, extra_data) do - container = container |> Repo.preload([:ammo_groups, :tags]) - columns |> Map.new(fn %{key: key} -> {key, get_value_for_key(key, container, extra_data)} end) end @@ -120,18 +122,24 @@ defmodule CanneryWeb.Components.ContainerTableComponent do """} end - defp get_value_for_key(:packs, container, _extra_data) do - container |> Containers.get_container_ammo_group_count!() + defp get_value_for_key(:packs, %{id: container_id}, %{pack_count: pack_count}) do + pack_count |> Map.get(container_id, 0) end - defp get_value_for_key(:rounds, container, _extra_data) do - container |> Containers.get_container_rounds!() + defp get_value_for_key(:rounds, %{id: container_id}, %{round_count: round_count}) do + round_count |> Map.get(container_id, 0) end defp get_value_for_key(:tags, container, %{tag_actions: tag_actions}) do assigns = %{tag_actions: tag_actions, container: container} - {container.tags |> Enum.map(fn %{name: name} -> name end), + tag_names = + container.tags + |> Enum.map(fn %{name: name} -> name end) + |> Enum.sort() + |> Enum.join(" ") + + {tag_names, ~H"""
<%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %>
- <%= if @code_actions, do: render_slot(@code_actions) %>
- - <%= case Application.get_env(:cannery, Cannery.Accounts)[:registration] do - "public" -> gettext("Public Signups") - _ -> gettext("Invite Only") + <%= case Accounts.registration_mode() do + :public -> gettext("Public Signups") + :invite_only -> gettext("Invite Only") end %>