diff --git a/.drone.yml b/.drone.yml index 4737424..54422fc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -32,9 +32,13 @@ steps: - mix test - name: build and publish stable - image: plugins/docker + image: thegeeklab/drone-docker-buildx + privileged: true settings: repo: shibaobun/cannery + purge: true + compress: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 username: from_secret: docker_username password: @@ -45,9 +49,13 @@ steps: - stable - name: build and publish tagged version - image: plugins/docker + image: thegeeklab/drone-docker-buildx + privileged: true settings: repo: shibaobun/cannery + purge: true + compress: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 username: from_secret: docker_username password: diff --git a/CHANGELOG.md b/CHANGELOG.md index 811898b..5098b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# v0.5.5 +- Forgot to add the logo as the favicon whoops + +# v0.5.4 +- Rename "Ammo" tab to "Catalog", and "Manage" tab is now "Ammo" +- Ammo groups are now just referred to as Ammo or "Packs" +- URL paths now reflect new names +- Add pack and round count to container information +- Add cute logo >:3 Thank you [kalli](https://twitter.com/t0kkuro)! +- Add note about deleting an ammo type deleting all ammo of that type as well +- Prompt to create first ammo type before trying to create first ammo +- Add note about creating unlimited invites +- Update screenshot lol + # v0.5.3 - Update French translation: Thank you [duponin](https://udongein.xyz/users/duponin)! - Update German translation: Thank you [Kaia](https://shitposter.club/users/kaia)! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1605e6..9632e55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,3 +143,4 @@ Thank you so much for your contributions! - shibao (https://misskey.bubbletea.dev/@shibao) - kaia (https://shitposter.club/users/kaia) - duponin (https://udongein.xyz/users/duponin) +- kalli (https://twitter.com/t0kkuro) diff --git a/README.md b/README.md index 6102d3a..30a5eed 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cannery -![screenshot](https://gitea.bubbletea.dev/shibao/cannery/raw/branch/stable/home.png) +![old screenshot](https://gitea.bubbletea.dev/shibao/cannery/raw/branch/stable/home.png) The self-hosted firearm tracker website. diff --git a/assets/static/images/cannery.png b/assets/static/images/cannery.png new file mode 100644 index 0000000..fe2652a Binary files /dev/null and b/assets/static/images/cannery.png differ diff --git a/assets/static/images/cannery.svg b/assets/static/images/cannery.svg new file mode 100644 index 0000000..e10f526 --- /dev/null +++ b/assets/static/images/cannery.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/home.png b/home.png index ef8b69c..672d205 100644 Binary files a/home.png and b/home.png differ diff --git a/lib/cannery/activity_log.ex b/lib/cannery/activity_log.ex index 354872d..911ee67 100644 --- a/lib/cannery/activity_log.ex +++ b/lib/cannery/activity_log.ex @@ -4,8 +4,7 @@ defmodule Cannery.ActivityLog do """ import Ecto.Query, warn: false - import CanneryWeb.Gettext - alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup, Repo} + alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo} alias Ecto.{Changeset, Multi} @doc """ @@ -60,32 +59,30 @@ defmodule Cannery.ActivityLog do """ @spec create_shot_group(attrs :: map(), User.t(), AmmoGroup.t()) :: {:ok, ShotGroup.t()} | {:error, Changeset.t(ShotGroup.t()) | nil} - def create_shot_group( - attrs, - %User{id: user_id}, - %AmmoGroup{id: ammo_group_id, count: ammo_group_count, user_id: user_id} = ammo_group - ) do - attrs = attrs |> Map.merge(%{"user_id" => user_id, "ammo_group_id" => ammo_group_id}) - changeset = %ShotGroup{} |> ShotGroup.create_changeset(attrs) - shot_group_count = changeset |> Changeset.get_field(:count) - - if shot_group_count > ammo_group_count do - error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count) - changeset = changeset |> Changeset.add_error(:count, error) - {:error, changeset} - else - Multi.new() - |> Multi.insert(:create_shot_group, changeset) - |> Multi.update( - :update_ammo_group, - ammo_group |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_group_count}) - ) - |> Repo.transaction() - |> case do - {:ok, %{create_shot_group: shot_group}} -> {:ok, shot_group} - {:error, :create_shot_group, changeset, _changes_so_far} -> {:error, changeset} - {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} + def create_shot_group(attrs, user, ammo_group) do + Multi.new() + |> Multi.insert( + :create_shot_group, + %ShotGroup{} |> ShotGroup.create_changeset(user, ammo_group, attrs) + ) + |> 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)} end + ) + |> Multi.update( + :update_ammo_group, + fn %{create_shot_group: %{count: shot_group_count}, ammo_group: %{count: ammo_group_count}} -> + ammo_group |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_group_count}) + end + ) + |> Repo.transaction() + |> case do + {:ok, %{create_shot_group: shot_group}} -> {:ok, shot_group} + {:error, :create_shot_group, changeset, _changes_so_far} -> {:error, changeset} + {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} end end @@ -104,42 +101,38 @@ defmodule Cannery.ActivityLog do @spec update_shot_group(ShotGroup.t(), attrs :: map(), User.t()) :: {:ok, ShotGroup.t()} | {:error, Changeset.t(ShotGroup.t()) | nil} def update_shot_group( - %ShotGroup{count: count, user_id: user_id, ammo_group_id: ammo_group_id} = shot_group, + %ShotGroup{count: count, user_id: user_id} = shot_group, attrs, %User{id: user_id} = user ) do - %{count: ammo_group_count, user_id: ^user_id} = - ammo_group = ammo_group_id |> Ammo.get_ammo_group!(user) - - changeset = shot_group |> ShotGroup.update_changeset(attrs) - new_shot_group_count = changeset |> Changeset.get_field(:count) - shot_diff_to_add = new_shot_group_count - count - - cond do - shot_diff_to_add > ammo_group_count -> - error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count) - changeset = changeset |> Changeset.add_error(:count, error) - {:error, changeset} - - new_shot_group_count <= 0 -> - error = dgettext("errors", "Count must be at least 1") - changeset = changeset |> Changeset.add_error(:count, error) - {:error, changeset} - - true -> - Multi.new() - |> Multi.update(:update_shot_group, changeset) - |> Multi.update( - :update_ammo_group, - ammo_group - |> AmmoGroup.range_changeset(%{"count" => ammo_group_count - shot_diff_to_add}) - ) - |> Repo.transaction() - |> case do - {:ok, %{update_shot_group: shot_group}} -> {:ok, shot_group} - {:error, :update_shot_group, changeset, _changes_so_far} -> {:error, changeset} - {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} - end + Multi.new() + |> Multi.update( + :update_shot_group, + shot_group |> ShotGroup.update_changeset(user, attrs) + ) + |> Multi.run( + :ammo_group, + fn repo, %{update_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)} + end + ) + |> Multi.update( + :update_ammo_group, + fn %{ + update_shot_group: %{count: new_count}, + ammo_group: %{count: ammo_group_count} = ammo_group + } -> + shot_diff_to_add = new_count - count + new_ammo_group_count = ammo_group_count - shot_diff_to_add + ammo_group |> AmmoGroup.range_changeset(%{"count" => new_ammo_group_count}) + end + ) + |> Repo.transaction() + |> case do + {:ok, %{update_shot_group: shot_group}} -> {:ok, shot_group} + {:error, :update_shot_group, changeset, _changes_so_far} -> {:error, changeset} + {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} end end @@ -158,18 +151,27 @@ defmodule Cannery.ActivityLog do @spec delete_shot_group(ShotGroup.t(), User.t()) :: {:ok, ShotGroup.t()} | {:error, Changeset.t(ShotGroup.t())} def delete_shot_group( - %ShotGroup{count: count, user_id: user_id, ammo_group_id: ammo_group_id} = shot_group, - %User{id: user_id} = user + %ShotGroup{user_id: user_id} = shot_group, + %User{id: user_id} ) do - %{count: ammo_group_count, user_id: ^user_id} = - ammo_group = ammo_group_id |> Ammo.get_ammo_group!(user) - Multi.new() |> Multi.delete(:delete_shot_group, shot_group) + |> Multi.run( + :ammo_group, + fn repo, %{delete_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)} + end + ) |> Multi.update( :update_ammo_group, - ammo_group - |> AmmoGroup.range_changeset(%{"count" => ammo_group_count + count}) + fn %{ + delete_shot_group: %{count: count}, + ammo_group: %{count: ammo_group_count} = ammo_group + } -> + new_ammo_group_count = ammo_group_count + count + ammo_group |> AmmoGroup.range_changeset(%{"count" => new_ammo_group_count}) + end ) |> Repo.transaction() |> case do @@ -178,21 +180,4 @@ defmodule Cannery.ActivityLog do {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil} end end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking shot_group changes. - - ## Examples - - iex> change_shot_group(shot_group) - %Ecto.Changeset{data: %ShotGroup{}} - - """ - @spec change_shot_group(ShotGroup.t() | ShotGroup.new_shot_group()) :: - Changeset.t(ShotGroup.t() | ShotGroup.new_shot_group()) - @spec change_shot_group(ShotGroup.t() | ShotGroup.new_shot_group(), attrs :: map()) :: - Changeset.t(ShotGroup.t() | ShotGroup.new_shot_group()) - def change_shot_group(%ShotGroup{} = shot_group, attrs \\ %{}) do - shot_group |> ShotGroup.update_changeset(attrs) - end end diff --git a/lib/cannery/activity_log/shot_group.ex b/lib/cannery/activity_log/shot_group.ex index 7b25433..f81c1fb 100644 --- a/lib/cannery/activity_log/shot_group.ex +++ b/lib/cannery/activity_log/shot_group.ex @@ -4,8 +4,9 @@ defmodule Cannery.ActivityLog.ShotGroup do """ use Ecto.Schema + import CanneryWeb.Gettext import Ecto.Changeset - alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup} + alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo.AmmoGroup, Repo} alias Ecto.{Changeset, UUID} @primary_key {:id, :binary_id, autogenerate: true} @@ -37,21 +38,84 @@ defmodule Cannery.ActivityLog.ShotGroup do @type id :: UUID.t() @doc false - @spec create_changeset(new_shot_group(), attrs :: map()) :: Changeset.t(new_shot_group()) - def create_changeset(shot_group, attrs) do + @spec create_changeset( + new_shot_group(), + User.t() | any(), + AmmoGroup.t() | any(), + attrs :: map() + ) :: + Changeset.t(new_shot_group()) + def create_changeset( + shot_group, + %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 shot_group - |> cast(attrs, [:count, :notes, :date, :ammo_group_id, :user_id]) + |> 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, :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]) + |> add_error(:invalid, dgettext("errors", "Please select a valid user and ammo group")) + 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}", count: ammo_group_count) + changeset |> Changeset.add_error(:count, error) + else + changeset + end + end + @doc false - @spec update_changeset(t() | new_shot_group(), attrs :: map()) :: + @spec update_changeset(t() | new_shot_group(), User.t(), attrs :: map()) :: Changeset.t(t() | new_shot_group()) - def update_changeset(shot_group, attrs) do + def update_changeset( + %ShotGroup{user_id: user_id} = shot_group, + %User{id: user_id} = user, + attrs + ) + when not (user_id |> is_nil()) do shot_group |> cast(attrs, [:count, :notes, :date]) |> validate_number(:count, greater_than: 0) |> validate_required([:count]) + |> validate_update_shot_group_count(shot_group, user) + end + + 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) + + new_shot_group_count = changeset |> Changeset.get_field(:count) + shot_diff_to_add = new_shot_group_count - count + + cond do + shot_diff_to_add > ammo_group_count -> + error = dgettext("errors", "Count must be less than %{count}", count: ammo_group_count) + changeset |> Changeset.add_error(:count, error) + + new_shot_group_count <= 0 -> + changeset |> Changeset.add_error(:count, dgettext("errors", "Count must be at least 1")) + + true -> + changeset + end end end diff --git a/lib/cannery/ammo.ex b/lib/cannery/ammo.ex index ea9c853..3ebe632 100644 --- a/lib/cannery/ammo.ex +++ b/lib/cannery/ammo.ex @@ -3,6 +3,7 @@ defmodule Cannery.Ammo do The Ammo context. """ + import CanneryWeb.Gettext import Ecto.Query, warn: false alias Cannery.{Accounts.User, Containers, Repo} alias Cannery.ActivityLog.ShotGroup @@ -24,6 +25,25 @@ defmodule Cannery.Ammo do def list_ammo_types(%User{id: user_id}), do: Repo.all(from at in AmmoType, where: at.user_id == ^user_id, order_by: at.name) + @doc """ + Returns a count of ammo_types. + + ## Examples + + iex> get_ammo_types_count!(%User{id: 123}) + 3 + + """ + @spec get_ammo_types_count!(User.t()) :: integer() + def get_ammo_types_count!(%User{id: user_id}) do + Repo.one( + from at in AmmoType, + where: at.user_id == ^user_id, + select: count(at.id), + distinct: true + ) + end + @doc """ Gets a single ammo_type. @@ -140,11 +160,8 @@ defmodule Cannery.Ammo do """ @spec create_ammo_type(attrs :: map(), User.t()) :: {:ok, AmmoType.t()} | {:error, Changeset.t(AmmoType.new_ammo_type())} - def create_ammo_type(attrs \\ %{}, %User{id: user_id}) do - %AmmoType{} - |> AmmoType.create_changeset(attrs |> Map.put("user_id", user_id)) - |> Repo.insert() - end + def create_ammo_type(attrs \\ %{}, %User{} = user), + do: %AmmoType{} |> AmmoType.create_changeset(user, attrs) |> Repo.insert() @doc """ Updates a ammo_type. @@ -193,22 +210,6 @@ defmodule Cannery.Ammo do def delete_ammo_type!(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}), do: ammo_type |> Repo.delete!() - @doc """ - Returns an `%Changeset{}` for tracking ammo_type changes. - - ## Examples - - iex> change_ammo_type(ammo_type) - %Changeset{data: %AmmoType{}} - - """ - @spec change_ammo_type(AmmoType.t() | AmmoType.new_ammo_type()) :: - Changeset.t(AmmoType.t() | AmmoType.new_ammo_type()) - @spec change_ammo_type(AmmoType.t() | AmmoType.new_ammo_type(), attrs :: map()) :: - Changeset.t(AmmoType.t() | AmmoType.new_ammo_type()) - def change_ammo_type(%AmmoType{} = ammo_type, attrs \\ %{}), - do: AmmoType.update_changeset(ammo_type, attrs) - @doc """ Returns the list of ammo_groups for a user and type. @@ -350,18 +351,21 @@ defmodule Cannery.Ammo do def create_ammo_groups( %{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs, multiplier, - %User{id: user_id} = user + %User{} = user ) - when multiplier >= 1 and multiplier <= @ammo_group_create_limit do - # validate ammo type and container ids belong to user - _valid_ammo_type = get_ammo_type!(ammo_type_id, user) - _valid_container = Containers.get_container!(container_id, user) - + when multiplier >= 1 and multiplier <= @ammo_group_create_limit and + not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) changesets = Enum.map(1..multiplier, fn _count -> - %AmmoGroup{} |> AmmoGroup.create_changeset(attrs |> Map.put("user_id", user_id)) + %AmmoGroup{} + |> AmmoGroup.create_changeset( + get_ammo_type!(ammo_type_id, user), + Containers.get_container!(container_id, user), + user, + attrs + ) end) if changesets |> Enum.all?(fn %{valid?: valid} -> valid end) do @@ -386,8 +390,27 @@ defmodule Cannery.Ammo do end end - def create_ammo_groups(invalid_attrs, _multiplier, _user) do - {:error, %AmmoGroup{} |> AmmoGroup.create_changeset(invalid_attrs)} + def create_ammo_groups( + %{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs, + _multiplier, + user + ) + when not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do + changeset = + %AmmoGroup{} + |> AmmoGroup.create_changeset( + get_ammo_type!(ammo_type_id, user), + Containers.get_container!(container_id, user), + user, + attrs + ) + |> Changeset.add_error(:multiplier, dgettext("errors", "Invalid multiplier")) + + {:error, changeset} + end + + def create_ammo_groups(invalid_attrs, _multiplier, user) do + {:error, %AmmoGroup{} |> AmmoGroup.create_changeset(nil, nil, user, invalid_attrs)} end @doc """ @@ -436,18 +459,4 @@ 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!() - - @doc """ - Returns an `%Changeset{}` for tracking ammo_group changes. - - ## Examples - - iex> change_ammo_group(ammo_group) - %Changeset{data: %AmmoGroup{}} - - """ - @spec change_ammo_group(AmmoGroup.t()) :: Changeset.t(AmmoGroup.t()) - @spec change_ammo_group(AmmoGroup.t(), attrs :: map()) :: Changeset.t(AmmoGroup.t()) - def change_ammo_group(%AmmoGroup{} = ammo_group, attrs \\ %{}), - do: AmmoGroup.update_changeset(ammo_group, attrs) end diff --git a/lib/cannery/ammo/ammo_group.ex b/lib/cannery/ammo/ammo_group.ex index 9e9954b..09426f0 100644 --- a/lib/cannery/ammo/ammo_group.ex +++ b/lib/cannery/ammo/ammo_group.ex @@ -7,6 +7,7 @@ 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.Container} @@ -48,22 +49,49 @@ defmodule Cannery.Ammo.AmmoGroup do @type id :: UUID.t() @doc false - @spec create_changeset(new_ammo_group(), attrs :: map()) :: Changeset.t(new_ammo_group()) - def create_changeset(ammo_group, attrs) do + @spec create_changeset( + new_ammo_group(), + AmmoType.t() | nil, + Container.t() | nil, + User.t(), + attrs :: map() + ) :: Changeset.t(new_ammo_group()) + def create_changeset( + ammo_group, + %AmmoType{id: ammo_type_id}, + %Container{id: container_id, user_id: user_id}, + %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 ammo_group - |> cast(attrs, [:count, :price_paid, :notes, :staged, :ammo_type_id, :container_id, :user_id]) + |> change(ammo_type_id: ammo_type_id) + |> change(user_id: user_id) + |> change(container_id: container_id) + |> cast(attrs, [:count, :price_paid, :notes, :staged]) |> validate_number(:count, greater_than: 0) |> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id]) end + @doc """ + Invalid changeset, used to prompt user to select ammo type and container + """ + def create_changeset(ammo_group, _invalid_ammo_type, _invalid_container, _invalid_user, attrs) do + ammo_group + |> cast(attrs, [:ammo_type_id, :container_id]) + |> validate_required([:ammo_type_id, :container_id]) + |> add_error(:invalid, dgettext("errors", "Please select an ammo type and container")) + end + @doc false @spec update_changeset(t() | new_ammo_group(), attrs :: map()) :: Changeset.t(t() | new_ammo_group()) def update_changeset(ammo_group, attrs) do ammo_group - |> cast(attrs, [:count, :price_paid, :notes, :staged, :ammo_type_id, :container_id]) + |> cast(attrs, [:count, :price_paid, :notes, :staged]) |> validate_number(:count, greater_than_or_equal_to: 0) - |> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id]) + |> validate_required([:count, :staged]) end @doc """ @@ -75,6 +103,6 @@ defmodule Cannery.Ammo.AmmoGroup do def range_changeset(ammo_group, attrs) do ammo_group |> cast(attrs, [:count, :staged]) - |> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id]) + |> validate_required([:count, :staged]) end end diff --git a/lib/cannery/ammo/ammo_type.ex b/lib/cannery/ammo/ammo_type.ex index d986dcc..c6e5012 100644 --- a/lib/cannery/ammo/ammo_type.ex +++ b/lib/cannery/ammo/ammo_type.ex @@ -105,10 +105,12 @@ defmodule Cannery.Ammo.AmmoType do ] @doc false - @spec create_changeset(new_ammo_type(), attrs :: map()) :: Changeset.t(new_ammo_type()) - def create_changeset(ammo_type, attrs) do + @spec create_changeset(new_ammo_type(), User.t(), attrs :: map()) :: + Changeset.t(new_ammo_type()) + def create_changeset(ammo_type, %User{id: user_id}, attrs) do ammo_type - |> cast(attrs, [:user_id | changeset_fields()]) + |> change(user_id: user_id) + |> cast(attrs, changeset_fields()) |> validate_required([:name, :user_id]) end @@ -118,6 +120,6 @@ defmodule Cannery.Ammo.AmmoType do def update_changeset(ammo_type, attrs) do ammo_type |> cast(attrs, changeset_fields()) - |> validate_required([:name, :user_id]) + |> validate_required(:name) end end diff --git a/lib/cannery/containers.ex b/lib/cannery/containers.ex index 55e0b90..ef4c378 100644 --- a/lib/cannery/containers.ex +++ b/lib/cannery/containers.ex @@ -30,6 +30,25 @@ defmodule Cannery.Containers do ) end + @doc """ + Returns a count of containers. + + ## Examples + + iex> get_containers_count!(%User{id: 123}) + 3 + + """ + @spec get_containers_count!(User.t()) :: integer() + def get_containers_count!(%User{id: user_id}) do + Repo.one( + from c in Container, + where: c.user_id == ^user_id, + select: count(c.id), + distinct: true + ) + end + @doc """ Gets a single container. @@ -71,9 +90,8 @@ defmodule Cannery.Containers do """ @spec create_container(attrs :: map(), User.t()) :: {:ok, Container.t()} | {:error, Changeset.t(Container.new_container())} - def create_container(attrs, %User{id: user_id}) do - attrs = attrs |> Map.put("user_id", user_id) - %Container{} |> Container.create_changeset(attrs) |> Repo.insert() + def create_container(attrs, %User{} = user) do + %Container{} |> Container.create_changeset(user, attrs) |> Repo.insert() end @doc """ @@ -122,7 +140,7 @@ defmodule Cannery.Containers do error = dgettext("errors", "Container must be empty before deleting") container - |> change_container() + |> Container.update_changeset(%{}) |> Changeset.add_error(:ammo_groups, error) |> Changeset.apply_action(:delete) end @@ -143,25 +161,6 @@ defmodule Cannery.Containers do container end - @doc """ - Returns an `%Changeset{}` for tracking container changes. - - ## Examples - - iex> change_container(container) - %Changeset{data: %Container{}} - - iex> change_container(%Changeset{}) - %Changeset{data: %Container{}} - - """ - @spec change_container(Container.t() | Container.new_container()) :: - Changeset.t(Container.t() | Container.new_container()) - @spec change_container(Container.t() | Container.new_container(), attrs :: map()) :: - Changeset.t(Container.t() | Container.new_container()) - def change_container(container, attrs \\ %{}), - do: container |> Container.update_changeset(attrs) - @doc """ Adds a tag to a container @@ -173,14 +172,11 @@ defmodule Cannery.Containers do """ @spec add_tag!(Container.t(), Tag.t(), User.t()) :: ContainerTag.t() def add_tag!( - %Container{id: container_id, user_id: user_id}, - %Tag{id: tag_id, user_id: user_id}, + %Container{user_id: user_id} = container, + %Tag{user_id: user_id} = tag, %User{id: user_id} - ) do - %ContainerTag{} - |> ContainerTag.changeset(%{"container_id" => container_id, "tag_id" => tag_id}) - |> Repo.insert!() - end + ), + do: %ContainerTag{} |> ContainerTag.create_changeset(tag, container) |> Repo.insert!() @doc """ Removes a tag from a container @@ -207,6 +203,18 @@ defmodule Cannery.Containers do if count == 0, do: raise("could not delete container tag"), else: count end + @doc """ + Returns number of rounds in container. If data is already preloaded, then + there will be no db hit. + """ + @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.count() + end + @doc """ Returns number of rounds in container. If data is already preloaded, then there will be no db hit. diff --git a/lib/cannery/containers/container.ex b/lib/cannery/containers/container.ex index c3ded85..c6bd8ae 100644 --- a/lib/cannery/containers/container.ex +++ b/lib/cannery/containers/container.ex @@ -42,10 +42,12 @@ defmodule Cannery.Containers.Container do @type id :: UUID.t() @doc false - @spec create_changeset(new_container(), attrs :: map()) :: Changeset.t(new_container()) - def create_changeset(container, attrs) do + @spec create_changeset(new_container(), User.t(), attrs :: map()) :: + Changeset.t(new_container()) + def create_changeset(container, %User{id: user_id}, attrs) do container - |> cast(attrs, [:name, :desc, :type, :location, :user_id]) + |> change(user_id: user_id) + |> cast(attrs, [:name, :desc, :type, :location]) |> validate_required([:name, :type, :user_id]) end @@ -55,6 +57,6 @@ defmodule Cannery.Containers.Container do def update_changeset(container, attrs) do container |> cast(attrs, [:name, :desc, :type, :location]) - |> validate_required([:name, :type, :user_id]) + |> validate_required([:name, :type]) end end diff --git a/lib/cannery/containers/container_tag.ex b/lib/cannery/containers/container_tag.ex index f8e68ed..cbefaa6 100644 --- a/lib/cannery/containers/container_tag.ex +++ b/lib/cannery/containers/container_tag.ex @@ -31,10 +31,16 @@ defmodule Cannery.Containers.ContainerTag do @type id :: UUID.t() @doc false - @spec changeset(new_container_tag(), attrs :: map()) :: Changeset.t(new_container_tag()) - def changeset(container_tag, attrs) do + @spec create_changeset(new_container_tag(), Tag.t(), Container.t()) :: + Changeset.t(new_container_tag()) + def create_changeset( + container_tag, + %Tag{id: tag_id, user_id: user_id}, + %Container{id: container_id, user_id: user_id} + ) do container_tag - |> cast(attrs, [:tag_id, :container_id]) + |> change(tag_id: tag_id) + |> change(container_id: container_id) |> validate_required([:tag_id, :container_id]) end end diff --git a/lib/cannery/invites.ex b/lib/cannery/invites.ex index f8788ad..e767408 100644 --- a/lib/cannery/invites.ex +++ b/lib/cannery/invites.ex @@ -100,15 +100,13 @@ defmodule Cannery.Invites do """ @spec create_invite(User.t(), attrs :: map()) :: {:ok, Invite.t()} | {:error, Changeset.t(Invite.new_invite())} - def create_invite(%User{id: user_id, role: :admin}, attrs) do + def create_invite(%User{role: :admin} = user, attrs) do token = :crypto.strong_rand_bytes(@invite_token_length) |> Base.url_encode64() |> binary_part(0, @invite_token_length) - attrs = attrs |> Map.merge(%{"user_id" => user_id, "token" => token}) - - %Invite{} |> Invite.create_changeset(attrs) |> Repo.insert() + %Invite{} |> Invite.create_changeset(user, token, attrs) |> Repo.insert() end @doc """ @@ -155,19 +153,4 @@ defmodule Cannery.Invites do """ @spec delete_invite!(Invite.t(), User.t()) :: Invite.t() def delete_invite!(invite, %User{role: :admin}), do: invite |> Repo.delete!() - - @doc """ - Returns an `%Changeset{}` for tracking invite changes. - - ## Examples - - iex> change_invite(invite) - %Changeset{data: %Invite{}} - - """ - @spec change_invite(Invite.t() | Invite.new_invite()) :: - Changeset.t(Invite.t() | Invite.new_invite()) - @spec change_invite(Invite.t() | Invite.new_invite(), attrs :: map()) :: - Changeset.t(Invite.t() | Invite.new_invite()) - def change_invite(invite, attrs \\ %{}), do: invite |> Invite.update_changeset(attrs) end diff --git a/lib/cannery/invites/invite.ex b/lib/cannery/invites/invite.ex index 7b606f1..d7aba15 100644 --- a/lib/cannery/invites/invite.ex +++ b/lib/cannery/invites/invite.ex @@ -38,10 +38,12 @@ defmodule Cannery.Invites.Invite do @type id :: UUID.t() @doc false - @spec create_changeset(new_invite(), attrs :: map()) :: Changeset.t(new_invite()) - def create_changeset(invite, attrs) do + @spec create_changeset(new_invite(), User.t(), token :: binary(), attrs :: map()) :: + Changeset.t(new_invite()) + def create_changeset(invite, %User{id: user_id}, token, attrs) do invite - |> cast(attrs, [:name, :token, :uses_left, :disabled_at, :user_id]) + |> change(token: token, user_id: user_id) + |> cast(attrs, [:name, :uses_left, :disabled_at]) |> validate_required([:name, :token, :user_id]) |> validate_number(:uses_left, greater_than_or_equal_to: 0) end @@ -51,7 +53,7 @@ defmodule Cannery.Invites.Invite do def update_changeset(invite, attrs) do invite |> cast(attrs, [:name, :uses_left, :disabled_at]) - |> validate_required([:name, :token, :user_id]) + |> validate_required([:name]) |> validate_number(:uses_left, greater_than_or_equal_to: 0) end end diff --git a/lib/cannery/tags.ex b/lib/cannery/tags.ex index 8b3e960..91069e3 100644 --- a/lib/cannery/tags.ex +++ b/lib/cannery/tags.ex @@ -74,8 +74,8 @@ defmodule Cannery.Tags do """ @spec create_tag(attrs :: map(), User.t()) :: {:ok, Tag.t()} | {:error, Changeset.t(Tag.new_tag())} - def create_tag(attrs, %User{id: user_id}), - do: %Tag{} |> Tag.create_changeset(attrs |> Map.put("user_id", user_id)) |> Repo.insert() + def create_tag(attrs, %User{} = user), + do: %Tag{} |> Tag.create_changeset(user, attrs) |> Repo.insert() @doc """ Updates a tag. @@ -121,20 +121,6 @@ defmodule Cannery.Tags do @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!() - @doc """ - Returns an `%Changeset{}` for tracking tag changes. - - ## Examples - - iex> change_tag(tag) - %Changeset{data: %Tag{}} - - """ - @spec change_tag(Tag.t() | Tag.new_tag()) :: Changeset.t(Tag.t() | Tag.new_tag()) - @spec change_tag(Tag.t() | Tag.new_tag(), attrs :: map()) :: - Changeset.t(Tag.t() | Tag.new_tag()) - def change_tag(tag, attrs \\ %{}), do: Tag.update_changeset(tag, attrs) - @doc """ Get a random tag bg_color in `#ffffff` hex format diff --git a/lib/cannery/tags/tag.ex b/lib/cannery/tags/tag.ex index 6056077..a0dd0d7 100644 --- a/lib/cannery/tags/tag.ex +++ b/lib/cannery/tags/tag.ex @@ -35,10 +35,11 @@ defmodule Cannery.Tags.Tag do @type id() :: UUID.t() @doc false - @spec create_changeset(new_tag(), attrs :: map()) :: Changeset.t(new_tag()) - def create_changeset(tag, attrs) do + @spec create_changeset(new_tag(), User.t(), attrs :: map()) :: Changeset.t(new_tag()) + def create_changeset(tag, %User{id: user_id}, attrs) do tag - |> cast(attrs, [:name, :bg_color, :text_color, :user_id]) + |> change(user_id: user_id) + |> cast(attrs, [:name, :bg_color, :text_color]) |> validate_required([:name, :bg_color, :text_color, :user_id]) end @@ -47,6 +48,6 @@ defmodule Cannery.Tags.Tag do def update_changeset(tag, attrs) do tag |> cast(attrs, [:name, :bg_color, :text_color]) - |> validate_required([:name, :bg_color, :text_color, :user_id]) + |> validate_required([:name, :bg_color, :text_color]) end end diff --git a/lib/cannery_web/components/add_shot_group_component.ex b/lib/cannery_web/components/add_shot_group_component.ex index 679e5f2..1500aaf 100644 --- a/lib/cannery_web/components/add_shot_group_component.ex +++ b/lib/cannery_web/components/add_shot_group_component.ex @@ -16,9 +16,10 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do }, Socket.t() ) :: {:ok, Socket.t()} - def update(%{ammo_group: _ammo_group, current_user: _current_user} = assigns, socket) do + def update(%{ammo_group: ammo_group, current_user: current_user} = assigns, socket) do changeset = - %ShotGroup{date: NaiveDateTime.utc_now(), count: 1} |> ActivityLog.change_shot_group() + %ShotGroup{date: NaiveDateTime.utc_now(), count: 1} + |> ShotGroup.create_changeset(current_user, ammo_group, %{}) {:ok, socket |> assign(assigns) |> assign(:changeset, changeset)} end @@ -27,21 +28,13 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do def handle_event( "validate", %{"shot_group" => shot_group_params}, - %{ - assigns: %{ - ammo_group: %AmmoGroup{id: ammo_group_id} = ammo_group, - current_user: %User{id: user_id} - } - } = socket + %{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket ) do - shot_group_params = - shot_group_params - |> process_params(ammo_group) - |> Map.merge(%{"ammo_group_id" => ammo_group_id, "user_id" => user_id}) + params = shot_group_params |> process_params(ammo_group) changeset = %ShotGroup{} - |> ActivityLog.change_shot_group(shot_group_params) + |> ShotGroup.create_changeset(current_user, ammo_group, params) |> Map.put(:action, :validate) {:noreply, socket |> assign(:changeset, changeset)} @@ -51,17 +44,12 @@ defmodule CanneryWeb.Components.AddShotGroupComponent do "save", %{"shot_group" => shot_group_params}, %{ - assigns: %{ - ammo_group: %{id: ammo_group_id} = ammo_group, - current_user: %{id: user_id} = current_user, - return_to: return_to - } + assigns: %{ammo_group: ammo_group, current_user: current_user, return_to: return_to} } = socket ) do socket = shot_group_params |> process_params(ammo_group) - |> Map.merge(%{"ammo_group_id" => ammo_group_id, "user_id" => user_id}) |> ActivityLog.create_shot_group(current_user, ammo_group) |> case do {:ok, _shot_group} -> diff --git a/lib/cannery_web/components/container_card.ex b/lib/cannery_web/components/container_card.ex index fc298e9..db3af29 100644 --- a/lib/cannery_web/components/container_card.ex +++ b/lib/cannery_web/components/container_card.ex @@ -45,7 +45,12 @@ defmodule CanneryWeb.Components.ContainerCard do <% end %> - <%= if @container.ammo_groups do %> + <%= unless @container.ammo_groups |> Enum.empty?() do %> + + <%= gettext("Packs:") %> + <%= @container |> Containers.get_container_ammo_group_count!() %> + + <%= gettext("Rounds:") %> <%= @container |> Containers.get_container_rounds!() %> diff --git a/lib/cannery_web/components/move_ammo_group_component.ex b/lib/cannery_web/components/move_ammo_group_component.ex index 8a8c3de..06f6b03 100644 --- a/lib/cannery_web/components/move_ammo_group_component.ex +++ b/lib/cannery_web/components/move_ammo_group_component.ex @@ -22,7 +22,7 @@ defmodule CanneryWeb.Components.MoveAmmoGroupComponent do assigns, socket ) do - changeset = Ammo.change_ammo_group(ammo_group) + changeset = ammo_group |> AmmoGroup.update_changeset(%{}) containers = Containers.list_containers(current_user) diff --git a/lib/cannery_web/components/topbar.ex b/lib/cannery_web/components/topbar.ex index 0936510..5843838 100644 --- a/lib/cannery_web/components/topbar.ex +++ b/lib/cannery_web/components/topbar.ex @@ -16,10 +16,16 @@ defmodule CanneryWeb.Components.Topbar do