From a6b2c6181e8f12f50d9bcee43fad07d5a923b58c Mon Sep 17 00:00:00 2001 From: shibao Date: Thu, 24 Feb 2022 00:16:23 -0500 Subject: [PATCH] add multiple ammo groups at one time --- CHANGELOG.md | 1 + lib/cannery/ammo.ex | 61 ++++++++++++---- .../live/ammo_group_live/form_component.ex | 67 +++++++++++++++-- .../ammo_group_live/form_component.html.heex | 30 ++++++-- priv/gettext/actions.pot | 7 +- priv/gettext/default.pot | 5 ++ priv/gettext/errors.pot | 10 +++ priv/gettext/prompts.pot | 21 ++++-- test/cannery/activity_log_test.exs | 4 +- test/cannery/ammo_test.exs | 12 +-- .../cannery_web/live/ammo_group_live_test.exs | 73 +++++++++++++++++-- test/cannery_web/live/range_live_test.exs | 4 +- test/support/fixtures.ex | 16 +++- 13 files changed, 257 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 928d0ce..e0cdc45 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 186cdd8..094307d 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 f05b316..a87be40 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 5e2d8ef..3fe8bf2 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 5557e58..4c3eebe 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 e059534..0dab71e 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 68fc268..ec2f671 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 62f6964..261e5ac 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 c314ab4..d96c105 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 ad148fe..2ae4e6e 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 c8bd624..24beaad 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 4c05585..888ea0c 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 f7ee0e2..07bb7b2 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