diff --git a/CHANGELOG.md b/CHANGELOG.md
index 928d0ce5..e0cdc456 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
- Add prompt to create first container before first ammo group
- Edit and delete shot groups from ammo group show page
- Use today's date when adding new shot groups
+- Create multiple ammo groups at one time
# v0.2.3
- Fix modals with overflowing forms
diff --git a/lib/cannery/ammo.ex b/lib/cannery/ammo.ex
index 186cdd88..094307da 100644
--- a/lib/cannery/ammo.ex
+++ b/lib/cannery/ammo.ex
@@ -6,7 +6,9 @@ defmodule Cannery.Ammo do
import Ecto.Query, warn: false
alias Cannery.{Accounts.User, Containers, Repo}
alias Cannery.Ammo.{AmmoGroup, AmmoType}
- alias Ecto.Changeset
+ alias Ecto.{Changeset, Multi}
+
+ @ammo_group_create_limit 10_000
@doc """
Returns the list of ammo_types.
@@ -327,36 +329,63 @@ defmodule Cannery.Ammo do
end
@doc """
- Creates a ammo_group.
+ Creates multiple ammo_groups at once.
## Examples
- iex> create_ammo_group(%{field: value}, %User{id: 123})
- {:ok, %AmmoGroup{}}
+ iex> create_ammo_groups(%{field: value}, 3, %User{id: 123})
+ {:ok, {3, [%AmmoGroup{}]}}
- iex> create_ammo_group(%{field: bad_value}, %User{id: 123})
+ iex> create_ammo_groups(%{field: bad_value}, 3, %User{id: 123})
{:error, %Changeset{}}
"""
- @spec create_ammo_group(attrs :: map(), User.t()) ::
- {:ok, AmmoGroup.t()} | {:error, Changeset.t(AmmoGroup.new_ammo_group())}
- def create_ammo_group(
+ @spec create_ammo_groups(attrs :: map(), multiplier :: non_neg_integer(), User.t()) ::
+ {:ok, {count :: non_neg_integer(), [AmmoGroup.t()] | nil}}
+ | {:error, Changeset.t(AmmoGroup.new_ammo_group()) | nil}
+ def create_ammo_groups(
%{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs,
+ multiplier,
%User{id: user_id} = user
- ) do
+ )
+ 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)
- %AmmoGroup{}
- |> AmmoGroup.create_changeset(attrs |> Map.put("user_id", user_id))
- |> Repo.insert()
+ 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))
+ end)
+
+ if changesets |> Enum.all?(fn %{valid?: valid} -> valid end) do
+ Multi.new()
+ |> Multi.insert_all(
+ :create_ammo_groups,
+ AmmoGroup,
+ changesets
+ |> Enum.map(fn changeset ->
+ changeset
+ |> Map.get(:changes)
+ |> Map.merge(%{inserted_at: now, updated_at: now})
+ end),
+ returning: true
+ )
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{create_ammo_groups: {count, ammo_groups}}} -> {:ok, {count, ammo_groups}}
+ {:error, :create_ammo_groups, changeset, _changes_so_far} -> {:error, changeset}
+ {:error, _other_transaction, _value, _changes_so_far} -> {:error, nil}
+ end
+ else
+ {:error, changesets |> List.first()}
+ end
end
- def create_ammo_group(invalid_attrs, _user) do
- %AmmoGroup{}
- |> AmmoGroup.create_changeset(invalid_attrs |> Map.put("user_id", "-1"))
- |> Repo.insert()
+ def create_ammo_groups(invalid_attrs, _multiplier, _user) do
+ {:error, %AmmoGroup{} |> AmmoGroup.create_changeset(invalid_attrs)}
end
@doc """
diff --git a/lib/cannery_web/live/ammo_group_live/form_component.ex b/lib/cannery_web/live/ammo_group_live/form_component.ex
index f05b316e..a87be408 100644
--- a/lib/cannery_web/live/ammo_group_live/form_component.ex
+++ b/lib/cannery_web/live/ammo_group_live/form_component.ex
@@ -9,6 +9,8 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
alias Ecto.Changeset
alias Phoenix.LiveView.Socket
+ @ammo_group_create_limit 10_000
+
@impl true
@spec update(
%{:ammo_group => AmmoGroup.t(), :current_user => User.t(), optional(any) => any},
@@ -22,6 +24,7 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
def update(%{assigns: %{ammo_group: ammo_group, current_user: current_user}} = socket) do
socket =
socket
+ |> assign(:ammo_group_create_limit, @ammo_group_create_limit)
|> assign(:changeset, Ammo.change_ammo_group(ammo_group))
|> assign(:ammo_types, Ammo.list_ammo_types(current_user))
|> assign_new(:containers, fn -> Containers.list_containers(current_user) end)
@@ -80,20 +83,68 @@ defmodule CanneryWeb.AmmoGroupLive.FormComponent do
end
defp save_ammo_group(
- %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
+ %{assigns: %{changeset: changeset}} = socket,
:new,
- ammo_group_params
+ %{"multiplier" => multiplier_str} = ammo_group_params
) do
socket =
- case Ammo.create_ammo_group(ammo_group_params, current_user) do
- {:ok, _ammo_group} ->
- prompt = dgettext("prompts", "Ammo group created successfully")
- socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
+ case multiplier_str |> Integer.parse() do
+ {multiplier, _remainder}
+ when multiplier >= 1 and multiplier <= @ammo_group_create_limit ->
+ socket |> create_multiple(ammo_group_params, multiplier)
- {:error, %Changeset{} = changeset} ->
- socket |> assign(changeset: changeset)
+ {multiplier, _remainder} ->
+ error_msg =
+ dgettext(
+ "errors",
+ "Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}",
+ max: @ammo_group_create_limit,
+ multiplier: multiplier
+ )
+
+ {:error, changeset} =
+ changeset
+ |> Changeset.add_error(:multiplier, error_msg)
+ |> Changeset.apply_action(:insert)
+
+ socket |> assign(:changeset, changeset)
+
+ :error ->
+ error_msg = dgettext("errors", "Could not parse number of copies")
+
+ {:error, changeset} =
+ changeset
+ |> Changeset.add_error(:multiplier, error_msg)
+ |> Changeset.apply_action(:insert)
+
+ socket |> assign(:changeset, changeset)
end
{:noreply, socket}
end
+
+ defp create_multiple(
+ %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
+ ammo_group_params,
+ multiplier
+ ) do
+ case Ammo.create_ammo_groups(ammo_group_params, multiplier, current_user) do
+ {:ok, {count, _ammo_groups}} ->
+ prompt =
+ dngettext(
+ "prompts",
+ "Ammo group created successfully",
+ "Ammo groups created successfully",
+ count
+ )
+
+ socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
+
+ {:error, %Changeset{} = changeset} ->
+ socket |> assign(changeset: changeset)
+
+ {:error, nil} ->
+ socket
+ end
+ end
end
diff --git a/lib/cannery_web/live/ammo_group_live/form_component.html.heex b/lib/cannery_web/live/ammo_group_live/form_component.html.heex
index 5e2d8efc..3fe8bf2d 100644
--- a/lib/cannery_web/live/ammo_group_live/form_component.html.heex
+++ b/lib/cannery_web/live/ammo_group_live/form_component.html.heex
@@ -33,7 +33,7 @@
<%= label(f, :price_paid, gettext("Price paid"), class: "title text-lg text-primary-600") %>
<%= number_input(f, :price_paid,
- step: "0.01",
+ step: 0.01,
class: "text-center col-span-2 input input-primary"
) %>
<%= error_tag(f, :price_paid, "col-span-3 text-center") %>
@@ -51,9 +51,29 @@
) %>
<%= error_tag(f, :container_id, "col-span-3 text-center") %>
- <%= submit(dgettext("actions", "Save"),
- phx_disable_with: dgettext("prompts", "Saving..."),
- class: "mx-auto col-span-3 btn btn-primary"
- ) %>
+ <%= case @action do %>
+ <% :new -> %>
+
+
+ <%= label(f, :multiplier, gettext("Copies"), class: "title text-lg text-primary-600") %>
+ <%= number_input(f, :multiplier,
+ max: @ammo_group_create_limit,
+ class: "text-center input input-primary",
+ value: 1,
+ phx_update: "ignore"
+ ) %>
+
+ <%= submit(dgettext("actions", "Create"),
+ phx_disable_with: dgettext("prompts", "Creating..."),
+ class: "mx-auto btn btn-primary"
+ ) %>
+
+ <%= error_tag(f, :multiplier, "col-span-3 text-center") %>
+ <% :edit -> %>
+ <%= submit(dgettext("actions", "Save"),
+ phx_disable_with: dgettext("prompts", "Saving..."),
+ class: "mx-auto col-span-3 btn btn-primary"
+ ) %>
+ <% end %>
diff --git a/priv/gettext/actions.pot b/priv/gettext/actions.pot
index 5557e589..4c3eebef 100644
--- a/priv/gettext/actions.pot
+++ b/priv/gettext/actions.pot
@@ -125,7 +125,7 @@ msgstr ""
#, elixir-autogen, elixir-format
#: lib/cannery_web/components/add_shot_group_component.html.heex:46
-#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:54
+#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:73
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:156
#: lib/cannery_web/live/container_live/form_component.html.heex:50
#: lib/cannery_web/live/invite_live/form_component.html.heex:28
@@ -196,3 +196,8 @@ msgstr ""
#: lib/cannery_web/live/ammo_group_live/index.html.heex:36
msgid "add a container first"
msgstr ""
+
+#, elixir-autogen, elixir-format
+#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:66
+msgid "Create"
+msgstr ""
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index e059534d..0dab71e4 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -834,3 +834,8 @@ msgstr ""
#: lib/cannery_web/live/range_live/index.ex:28
msgid "Record Shots"
msgstr ""
+
+#, elixir-autogen, elixir-format
+#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:58
+msgid "Copies"
+msgstr ""
diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot
index 68fc2680..ec2f6718 100644
--- a/priv/gettext/errors.pot
+++ b/priv/gettext/errors.pot
@@ -157,3 +157,13 @@ msgstr ""
#: lib/cannery_web/live/container_live/edit_tags_component.ex:52
msgid "Tag could not be removed"
msgstr ""
+
+#, elixir-autogen, elixir-format
+#: lib/cannery_web/live/ammo_group_live/form_component.ex:113
+msgid "Could not parse number of copies"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+#: lib/cannery_web/live/ammo_group_live/form_component.ex:98
+msgid "Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}"
+msgstr ""
diff --git a/priv/gettext/prompts.pot b/priv/gettext/prompts.pot
index 62f6964b..261e5acb 100644
--- a/priv/gettext/prompts.pot
+++ b/priv/gettext/prompts.pot
@@ -61,11 +61,6 @@ msgstr ""
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
-#, elixir-autogen, elixir-format
-#: lib/cannery_web/live/ammo_group_live/form_component.ex:90
-msgid "Ammo group created successfully"
-msgstr ""
-
#, elixir-autogen, elixir-format
#: lib/cannery_web/live/ammo_group_live/index.ex:56
#: lib/cannery_web/live/ammo_group_live/show.ex:52
@@ -73,7 +68,7 @@ msgid "Ammo group deleted succesfully"
msgstr ""
#, elixir-autogen, elixir-format
-#: lib/cannery_web/live/ammo_group_live/form_component.ex:72
+#: lib/cannery_web/live/ammo_group_live/form_component.ex:75
msgid "Ammo group updated successfully"
msgstr ""
@@ -160,7 +155,7 @@ msgstr ""
#, elixir-autogen, elixir-format
#: lib/cannery_web/components/add_shot_group_component.html.heex:48
-#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:55
+#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:74
#: lib/cannery_web/live/ammo_type_live/form_component.html.heex:157
#: lib/cannery_web/live/container_live/form_component.html.heex:52
#: lib/cannery_web/live/invite_live/form_component.html.heex:30
@@ -251,3 +246,15 @@ msgstr ""
#: lib/cannery_web/live/ammo_group_live/index.html.heex:33
msgid "You'll need to"
msgstr ""
+
+#, elixir-autogen, elixir-format
+#: lib/cannery_web/live/ammo_group_live/form_component.html.heex:67
+msgid "Creating..."
+msgstr ""
+
+#, elixir-autogen, elixir-format
+#: lib/cannery_web/live/ammo_group_live/form_component.ex:134
+msgid "Ammo group created successfully"
+msgid_plural "Ammo groups created successfully"
+msgstr[0] ""
+msgstr[1] ""
diff --git a/test/cannery/activity_log_test.exs b/test/cannery/activity_log_test.exs
index c314ab4f..d96c105b 100644
--- a/test/cannery/activity_log_test.exs
+++ b/test/cannery/activity_log_test.exs
@@ -20,8 +20,8 @@ defmodule Cannery.ActivityLogTest do
container = container_fixture(current_user)
ammo_type = ammo_type_fixture(current_user)
- %{id: ammo_group_id} =
- ammo_group = ammo_group_fixture(%{"count" => 25}, ammo_type, container, current_user)
+ {1, [%{id: ammo_group_id} = ammo_group]} =
+ ammo_group_fixture(%{"count" => 25}, ammo_type, container, current_user)
shot_group =
%{"count" => 5, "date" => ~N[2022-02-13 03:17:00], "notes" => "some notes"}
diff --git a/test/cannery/ammo_test.exs b/test/cannery/ammo_test.exs
index ad148fe9..2ae4e6ec 100644
--- a/test/cannery/ammo_test.exs
+++ b/test/cannery/ammo_test.exs
@@ -108,7 +108,7 @@ defmodule Cannery.AmmoTest do
current_user = user_fixture()
ammo_type = ammo_type_fixture(current_user)
container = container_fixture(current_user)
- ammo_group = ammo_group_fixture(ammo_type, container, current_user)
+ {1, [ammo_group]} = ammo_group_fixture(ammo_type, container, current_user)
[
ammo_type: ammo_type,
@@ -129,28 +129,28 @@ defmodule Cannery.AmmoTest do
ammo_group |> Repo.preload(:shot_groups)
end
- test "create_ammo_group/1 with valid data creates a ammo_group",
+ test "create_ammo_groups/3 with valid data creates a ammo_group",
%{
ammo_type: ammo_type,
container: container,
current_user: current_user
} do
- assert {:ok, %AmmoGroup{} = ammo_group} =
+ assert {:ok, {1, [%AmmoGroup{} = ammo_group]}} =
@valid_attrs
|> Map.merge(%{"ammo_type_id" => ammo_type.id, "container_id" => container.id})
- |> Ammo.create_ammo_group(current_user)
+ |> Ammo.create_ammo_groups(1, current_user)
assert ammo_group.count == 42
assert ammo_group.notes == "some notes"
assert ammo_group.price_paid == 120.5
end
- test "create_ammo_group/1 with invalid data returns error changeset",
+ test "create_ammo_groups/3 with invalid data returns error changeset",
%{ammo_type: ammo_type, container: container, current_user: current_user} do
assert {:error, %Changeset{}} =
@invalid_attrs
|> Map.merge(%{"ammo_type_id" => ammo_type.id, "container_id" => container.id})
- |> Ammo.create_ammo_group(current_user)
+ |> Ammo.create_ammo_groups(1, current_user)
end
test "update_ammo_group/2 with valid data updates the ammo_group",
diff --git a/test/cannery_web/live/ammo_group_live_test.exs b/test/cannery_web/live/ammo_group_live_test.exs
index c8bd6249..24beaad2 100644
--- a/test/cannery_web/live/ammo_group_live_test.exs
+++ b/test/cannery_web/live/ammo_group_live_test.exs
@@ -6,19 +6,20 @@ defmodule CanneryWeb.AmmoGroupLiveTest do
use CanneryWeb.ConnCase
import Phoenix.LiveViewTest
import CanneryWeb.Gettext
- alias Cannery.Repo
+ alias Cannery.{Ammo, Repo}
@moduletag :ammo_group_live_test
@shot_group_create_attrs %{"ammo_left" => 5, "notes" => "some notes"}
@shot_group_update_attrs %{"count" => 5, "notes" => "some updated notes"}
- @create_attrs %{count: 42, notes: "some notes", price_paid: 120.5}
- @update_attrs %{count: 43, notes: "some updated notes", price_paid: 456.7}
+ @create_attrs %{"count" => 42, "notes" => "some notes", "price_paid" => 120.5}
+ @update_attrs %{"count" => 43, "notes" => "some updated notes", "price_paid" => 456.7}
+ @ammo_group_create_limit 10_000
# @invalid_attrs %{count: -1, notes: nil, price_paid: nil}
defp create_ammo_group(%{current_user: current_user}) do
ammo_type = ammo_type_fixture(current_user)
container = container_fixture(current_user)
- ammo_group = ammo_group_fixture(ammo_type, container, current_user)
+ {1, [ammo_group]} = ammo_group_fixture(ammo_type, container, current_user)
shot_group =
%{"count" => 5, "date" => ~N[2022-02-13 03:17:00], "notes" => "some notes"}
@@ -38,7 +39,7 @@ defmodule CanneryWeb.AmmoGroupLiveTest do
assert html =~ ammo_group.ammo_type.name
end
- test "saves new ammo_group", %{conn: conn} do
+ test "saves a single new ammo_group", %{conn: conn} do
{:ok, index_live, _html} = live(conn, Routes.ammo_group_index_path(conn, :index))
assert index_live |> element("a", dgettext("actions", "New Ammo group")) |> render_click() =~
@@ -60,6 +61,68 @@ defmodule CanneryWeb.AmmoGroupLiveTest do
assert html =~ "42"
end
+ test "saves multiple new ammo_groups", %{conn: conn, current_user: current_user} do
+ multiplier = 25
+
+ {:ok, index_live, _html} = live(conn, Routes.ammo_group_index_path(conn, :index))
+
+ assert index_live |> element("a", dgettext("actions", "New Ammo group")) |> render_click() =~
+ gettext("New Ammo group")
+
+ assert_patch(index_live, Routes.ammo_group_index_path(conn, :new))
+
+ # assert index_live
+ # |> form("#ammo_group-form", ammo_group: @invalid_attrs)
+ # |> render_change() =~ dgettext("errors", "can't be blank")
+
+ {:ok, _, html} =
+ index_live
+ |> form("#ammo_group-form",
+ ammo_group: @create_attrs |> Map.put("multiplier", to_string(multiplier))
+ )
+ |> render_submit()
+ |> follow_redirect(conn, Routes.ammo_group_index_path(conn, :index))
+
+ assert html =~ dgettext("prompts", "Ammo groups created successfully")
+ assert Ammo.list_ammo_groups(current_user) |> Enum.count() == multiplier + 1
+ end
+
+ test "does not save invalid number of new ammo_groups", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, Routes.ammo_group_index_path(conn, :index))
+
+ assert index_live |> element("a", dgettext("actions", "New Ammo group")) |> render_click() =~
+ gettext("New Ammo group")
+
+ assert_patch(index_live, Routes.ammo_group_index_path(conn, :new))
+
+ # assert index_live
+ # |> form("#ammo_group-form", ammo_group: @invalid_attrs)
+ # |> render_change() =~ dgettext("errors", "can't be blank")
+
+ assert index_live
+ |> form("#ammo_group-form", ammo_group: @create_attrs |> Map.put("multiplier", "0"))
+ |> render_submit() =~
+ dgettext(
+ "errors",
+ "Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}",
+ multiplier: 0,
+ max: @ammo_group_create_limit
+ )
+
+ assert index_live
+ |> form("#ammo_group-form",
+ ammo_group:
+ @create_attrs |> Map.put("multiplier", to_string(@ammo_group_create_limit + 1))
+ )
+ |> render_submit() =~
+ dgettext(
+ "errors",
+ "Invalid number of copies, must be between 1 and %{max}. Was %{multiplier}",
+ multiplier: @ammo_group_create_limit + 1,
+ max: @ammo_group_create_limit
+ )
+ end
+
test "saves new shot_group", %{conn: conn, ammo_group: ammo_group} do
{:ok, index_live, _html} = live(conn, Routes.ammo_group_index_path(conn, :index))
diff --git a/test/cannery_web/live/range_live_test.exs b/test/cannery_web/live/range_live_test.exs
index 4c05585f..888ea0ce 100644
--- a/test/cannery_web/live/range_live_test.exs
+++ b/test/cannery_web/live/range_live_test.exs
@@ -16,7 +16,9 @@ defmodule CanneryWeb.RangeLiveTest do
defp create_shot_group(%{current_user: current_user}) do
container = container_fixture(%{"staged" => true}, current_user)
ammo_type = ammo_type_fixture(current_user)
- ammo_group = ammo_group_fixture(%{"staged" => true}, ammo_type, container, current_user)
+
+ {1, [ammo_group]} =
+ ammo_group_fixture(%{"staged" => true}, ammo_type, container, current_user)
shot_group =
%{"count" => 5, "date" => ~N[2022-02-13 03:17:00], "notes" => "some notes"}
diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex
index f7ee0e21..07bb7b2f 100644
--- a/test/support/fixtures.ex
+++ b/test/support/fixtures.ex
@@ -111,10 +111,20 @@ defmodule Cannery.Fixtures do
@doc """
Generate a AmmoGroup
"""
- @spec ammo_group_fixture(AmmoType.t(), Container.t(), User.t()) :: AmmoGroup.t()
- @spec ammo_group_fixture(attrs :: map(), AmmoType.t(), Container.t(), User.t()) :: AmmoGroup.t()
+ @spec ammo_group_fixture(AmmoType.t(), Container.t(), User.t()) ::
+ {count :: non_neg_integer(), [AmmoGroup.t()]}
+ @spec ammo_group_fixture(attrs :: map(), AmmoType.t(), Container.t(), User.t()) ::
+ {count :: non_neg_integer(), [AmmoGroup.t()]}
+ @spec ammo_group_fixture(
+ attrs :: map(),
+ multiplier :: non_neg_integer(),
+ AmmoType.t(),
+ Container.t(),
+ User.t()
+ ) :: {count :: non_neg_integer(), [AmmoGroup.t()]}
def ammo_group_fixture(
attrs \\ %{},
+ multiplier \\ 1,
%AmmoType{id: ammo_type_id},
%Container{id: container_id},
%User{} = user
@@ -125,7 +135,7 @@ defmodule Cannery.Fixtures do
"container_id" => container_id,
"count" => 20
})
- |> Ammo.create_ammo_group(user)
+ |> Ammo.create_ammo_groups(multiplier, user)
|> unwrap_ok_tuple()
end