add range mode

This commit is contained in:
2022-02-15 17:33:45 -05:00
parent d9dd61b1a5
commit e4ef22184e
30 changed files with 1394 additions and 132 deletions

198
lib/cannery/activity_log.ex Normal file
View File

@ -0,0 +1,198 @@
defmodule Cannery.ActivityLog do
@moduledoc """
The ActivityLog context.
"""
import Ecto.Query, warn: false
import CanneryWeb.Gettext
alias Cannery.{Accounts.User, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup, Repo}
alias Ecto.{Changeset, Multi}
@doc """
Returns the list of shot_groups.
## Examples
iex> list_shot_groups(%User{id: 123})
[%ShotGroup{}, ...]
"""
@spec list_shot_groups(User.t()) :: [ShotGroup.t()]
def list_shot_groups(%User{id: user_id}) do
Repo.all(from(sg in ShotGroup, where: sg.user_id == ^user_id))
end
@doc """
Gets a single shot_group.
Raises `Ecto.NoResultsError` if the Shot group does not exist.
## Examples
iex> get_shot_group!(123, %User{id: 123})
%ShotGroup{}
iex> get_shot_group!(456, %User{id: 123})
** (Ecto.NoResultsError)
"""
@spec get_shot_group!(ShotGroup.id(), User.t()) :: ShotGroup.t()
def get_shot_group!(id, %User{id: user_id}) do
Repo.one!(
from sg in ShotGroup,
where: sg.id == ^id,
where: sg.user_id == ^user_id,
order_by: sg.date
)
end
@doc """
Creates a shot_group.
## Examples
iex> create_shot_group(%{field: value}, %User{id: 123})
{:ok, %ShotGroup{}}
iex> create_shot_group(%{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}}
"""
@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}
end
end
end
@doc """
Updates a shot_group.
## Examples
iex> update_shot_group(shot_group, %{field: new_value}, %User{id: 123})
{:ok, %ShotGroup{}}
iex> update_shot_group(shot_group, %{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}}
"""
@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,
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
end
end
@doc """
Deletes a shot_group.
## Examples
iex> delete_shot_group(shot_group, %User{id: 123})
{:ok, %ShotGroup{}}
iex> delete_shot_group(shot_group, %User{id: 123})
{:error, %Ecto.Changeset{}}
"""
@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
) 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.update(
:update_ammo_group,
ammo_group
|> AmmoGroup.range_changeset(%{"count" => ammo_group_count + count})
)
|> Repo.transaction()
|> case do
{:ok, %{delete_shot_group: shot_group}} -> {:ok, shot_group}
{:error, :delete_shot_group, changeset, _changes_so_far} -> {:error, changeset}
{: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

View File

@ -0,0 +1,57 @@
defmodule Cannery.ActivityLog.ShotGroup do
@moduledoc """
A shot group records a group of ammo shot during a range trip
"""
use Ecto.Schema
import Ecto.Changeset
alias Cannery.{Accounts.User, Ammo.AmmoGroup, ActivityLog.ShotGroup}
alias Ecto.{Changeset, UUID}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "shot_groups" do
field :count, :integer
field :date, :date
field :notes, :string
belongs_to :user, User
belongs_to :ammo_group, AmmoGroup
timestamps()
end
@type t :: %ShotGroup{
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 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
shot_group
|> cast(attrs, [:count, :notes, :date, :ammo_group_id, :user_id])
|> validate_number(:count, greater_than: 0)
|> validate_required([:count, :ammo_group_id, :user_id])
end
@doc false
@spec update_changeset(t() | new_shot_group(), attrs :: map()) ::
Changeset.t(t() | new_shot_group())
def update_changeset(shot_group, attrs) do
shot_group
|> cast(attrs, [:count, :notes, :date])
|> validate_number(:count, greater_than: 0)
|> validate_required([:count])
end
end

View File

@ -177,8 +177,28 @@ defmodule Cannery.Ammo do
"""
@spec list_ammo_groups(User.t()) :: [AmmoGroup.t()]
def list_ammo_groups(%User{id: user_id}) do
Repo.all(from am in AmmoGroup, where: am.user_id == ^user_id)
@spec list_ammo_groups(User.t(), include_empty :: boolean()) :: [AmmoGroup.t()]
def list_ammo_groups(%User{id: user_id}, include_empty \\ false) do
if include_empty do
from am in AmmoGroup, where: am.user_id == ^user_id
else
from am in AmmoGroup, where: am.user_id == ^user_id, where: not (am.count == 0)
end
|> Repo.all()
end
@doc """
Returns the list of staged ammo_groups for a user.
## Examples
iex> list_staged_ammo_groups(%User{id: 123})
[%AmmoGroup{}, ...]
"""
@spec list_staged_ammo_groups(User.t()) :: [AmmoGroup.t()]
def list_staged_ammo_groups(%User{id: user_id}) do
Repo.all(from am in AmmoGroup, where: am.user_id == ^user_id, where: am.staged == true)
end
@doc """

View File

@ -18,6 +18,7 @@ defmodule Cannery.Ammo.AmmoGroup do
field :count, :integer
field :notes, :string
field :price_paid, :float
field :staged, :boolean, default: false
belongs_to :ammo_type, AmmoType
belongs_to :container, Container
@ -31,6 +32,7 @@ defmodule Cannery.Ammo.AmmoGroup do
count: integer,
notes: String.t() | nil,
price_paid: float() | nil,
staged: boolean(),
ammo_type: AmmoType.t() | nil,
ammo_type_id: AmmoType.id(),
container: Container.t() | nil,
@ -47,9 +49,9 @@ defmodule Cannery.Ammo.AmmoGroup do
@spec create_changeset(new_ammo_group(), attrs :: map()) :: Changeset.t(new_ammo_group())
def create_changeset(ammo_group, attrs) do
ammo_group
|> cast(attrs, [:count, :price_paid, :notes, :ammo_type_id, :container_id, :user_id])
|> cast(attrs, [:count, :price_paid, :notes, :staged, :ammo_type_id, :container_id, :user_id])
|> validate_number(:count, greater_than: 0)
|> validate_required([:count, :ammo_type_id, :container_id, :user_id])
|> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id])
end
@doc false
@ -57,9 +59,9 @@ defmodule Cannery.Ammo.AmmoGroup do
Changeset.t(t() | new_ammo_group())
def update_changeset(ammo_group, attrs) do
ammo_group
|> cast(attrs, [:count, :price_paid, :notes, :ammo_type_id, :container_id])
|> cast(attrs, [:count, :price_paid, :notes, :staged, :ammo_type_id, :container_id])
|> validate_number(:count, greater_than: 0)
|> validate_required([:count, :ammo_type_id, :container_id, :user_id])
|> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id])
end
@doc """
@ -70,7 +72,7 @@ defmodule Cannery.Ammo.AmmoGroup do
Changeset.t(t() | new_ammo_group())
def range_changeset(ammo_group, attrs) do
ammo_group
|> cast(attrs, [:count])
|> validate_required([:count, :ammo_type_id, :container_id, :user_id])
|> cast(attrs, [:count, :staged])
|> validate_required([:count, :staged, :ammo_type_id, :container_id, :user_id])
end
end