From e3b91657ec91b8c8f27b3d3c5c631f92597282c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Thu, 27 Oct 2022 15:18:57 +0100 Subject: [PATCH 01/53] feat: Example UI done. --- lib/app_web/live/app_live.html.heex | 150 +++++++++++++++++++--------- 1 file changed, 103 insertions(+), 47 deletions(-) diff --git a/lib/app_web/live/app_live.html.heex b/lib/app_web/live/app_live.html.heex index b6395d34..959dd088 100644 --- a/lib/app_web/live/app_live.html.heex +++ b/lib/app_web/live/app_live.html.heex @@ -168,54 +168,110 @@ <%= if item.id == @editing do %> -
- - - - - -
- + + + + + + + + + +
+ + + + + + + +
+ +
+ + +
+

Timers

+ +
+
+
+

Start:

+ +
+
+

Start:

+ +
+ + + +
+
- +
+ + + + + + + + + + + <% else %>
From 211a93cfaa1d8ba659db48ca829ddb9d68063697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Fri, 4 Nov 2022 23:13:01 +0000 Subject: [PATCH 20/53] feat: Removing unnecessary list of timers when fetching items on mount. The timers are fetched ad-hoc when editing an item. --- lib/app/item.ex | 29 ----------------------------- lib/app_web/live/app_live.ex | 2 +- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/lib/app/item.ex b/lib/app/item.ex index 2d145adb..2d985496 100644 --- a/lib/app/item.ex +++ b/lib/app/item.ex @@ -161,38 +161,9 @@ defmodule App.Item do list_person_items(person_id) |> Enum.reduce(%{}, fn i, acc -> Map.put(acc, i.id, i) end) - items_timers = - Enum.group_by( - values, - fn row -> row.id end, - fn obj -> - start = - if obj.start != nil, - do: - NaiveDateTime.truncate(obj.start, :second) - |> NaiveDateTime.to_string(), - else: nil - - stop = - if obj.stop != nil, - do: - NaiveDateTime.truncate(obj.stop, :second) - |> NaiveDateTime.to_string(), - else: nil - - %{start: start, stop: stop, id: obj.timer_id} - end - ) - accumulate_item_timers(values) |> Enum.map(fn t -> Map.put(t, :tags, items_tags[t.id].tags) - |> Map.put( - :timers, - Enum.reject(items_timers[t.id], fn %{start: start, stop: stop} -> - start == nil and stop == nil - end) - ) end) end diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 7bc29121..ef74bd31 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -137,7 +137,7 @@ defmodule AppWeb.AppLive do case DateTime.compare(start, stop) do :lt -> Timer.update_timer(%{id: id, start: start, stop: stop}) - {:noreply, socket} + {:noreply, assign(socket, editing: nil, editing_timers: [])} :eq -> # Adding error to changeset From 45df2a1e5bdf50cfa0f094f2aca2bc1350e371e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Fri, 4 Nov 2022 23:21:50 +0000 Subject: [PATCH 21/53] fix: Fixing duplicate form when error occurs. https://elixirforum.com/t/i-had-the-weirdest-problem-with-liveviews-change-tracking-when-name-attribute-has-value-id/39008/3 --- lib/app_web/live/app_live.ex | 2 +- lib/app_web/live/app_live.html.heex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index ef74bd31..0ae5dc1d 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -122,7 +122,7 @@ defmodule AppWeb.AppLive do @impl true def handle_event( "update-item-timer", - %{"id" => id, "index" => index,"timer_start" => timer_start, "timer_stop" => timer_stop}, + %{"timer_id" => id, "index" => index,"timer_start" => timer_start, "timer_stop" => timer_stop}, socket ) do diff --git a/lib/app_web/live/app_live.html.heex b/lib/app_web/live/app_live.html.heex index 87ab3246..2d2d3865 100644 --- a/lib/app_web/live/app_live.html.heex +++ b/lib/app_web/live/app_live.html.heex @@ -258,7 +258,7 @@ value={changeset.data.stop} /> - + - - +
+
+ + + + + +
+ +
+
-
- <%= if (length item.timers) > 0 do %> -

Timers

- <% else %> -

- No timers associated with this item. -

- <% end %> - -
- <%= for timer <- item.timers do %> -
-
-
-

Start:

- -
-
-

Stop:

+
+ <%= if (length @editing_timers) > 0 do %> +

Timers

+ <% else %> +

+ No timers associated with this item. +

+ <% end %> + +
+ <%= @editing_timers |> Enum.with_index |> Enum.map(fn({changeset, index}) -> %> + <.form + :let={f} + for={changeset} + phx-submit="update-item-timer" + id={"form-update-timer-#{changeset.data.id}"} + class="w-full pr-2" + > +
+
+

Start:

+ +
+
+

Stop:

+ +
+ + +
- - - -
- - <% end %> + + <%= error_tag(f, :id) %> + + + <% end) %> +
-
@@ -3273,51 +3425,134 @@ We are showing each timer whenever an `item` is being edited. As you can see from the snippet above, when the changes from the form are submitted, a -`update-item-timer` event is created. Now we need to handle this event -in `lib/app_web/live/app_live.ex`. -Let's add the following method to the file. +`update-item-timer` event is created. + +## 12.4 Updating the tests and going back to 100% coverage +If we run `source .env_sample` and +`MIX_ENV=test mix coveralls.html ; open cover/excoveralls.html` +we will see how coverage dropped. +We need to test the new handler we created when updating a timer, +as well as the `update_timer` function added inside `timer.ex`. + +Paste the following test in `test/app/timer_test.exs`. ```elixir - def handle_event( - "update-item-timer", - %{"id" => id, "timer_start" => timer_start, "timer_stop" => timer_stop}, - socket - ) do - try do - start = App.DateTimeParser.parse!(timer_start, "%Y-%m-%d %H:%M:%S") - stop = App.DateTimeParser.parse!(timer_stop, "%Y-%m-%d %H:%M:%S") + test "update_timer(%{id: id, start: start, stop: stop}) should update the timer" do + start = ~N[2022-10-27 00:00:00] + stop = ~N[2022-10-27 05:00:00] - case DateTime.compare(start, stop) do - :lt -> Timer.update_timer(%{id: id, start: start, stop: stop}) - :eq -> Logger.debug("dates are the same") - :gt -> Logger.debug("Start is newer that stop") - end - rescue - e -> - Logger.debug( - "Date format invalid on either start or stop, #{inspect(e)}" - ) + {:ok, item} = Item.create_item(@valid_item_attrs) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + # Update timer to specific datetimes + Timer.update_timer(%{id: timer.id, start: start, stop: stop}) + + updated_timer = Timer.get_timer!(timer.id) + + assert updated_timer.start == start + assert updated_timer.stop == stop end +``` - AppWeb.Endpoint.broadcast(@topic, "update", :update) - {:noreply, socket} +We now test the newly created `update-item-timer` event. +In `test/app_web/live/app_live_test.exs`, add the following test. + +```elixir +test "update an item's timer", %{conn: conn} do + start = "2022-10-27T00:00:00" + stop = "2022-10-27T05:00:00" + start_datetime = ~N[2022-10-27 00:00:00] + stop_datetime = ~N[2022-10-27 05:00:00] + + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + {:ok, view, _html} = live(conn, "/") + + # Update successful + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => start, + "timer_stop" => stop + }) + + updated_timer = Timer.get_timer!(timer.id) + + assert updated_timer.start == start_datetime + assert updated_timer.stop == stop_datetime + + # Trying to update with equal values on start and stop + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => start, + "timer_stop" => start + }) =~ "Start or stop are equal." + + # Trying to update with equal start greater than stop + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => stop, + "timer_stop" => start + }) =~ "Start is newer that stop." + + # Trying to update with equal start greater than stop + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => "invalid", + "timer_stop" => "invalid" + }) =~ "Date format invalid on either start or stop." end ``` -In the piece of code above, we try to parse the input strings -and validate them. If it fails, we log the error. If they're -parseable, we compare the dates and make sure the `stop` time -is **greater** than `start`. If not, we log the errors. - -If everything is correct, the timer is updated and the -new `item` list is broadcasted to everyone on the -same channel so they have the updated `item` list, making -interaction *feel* real-time. +We also need to change the `test "edit-timer"` test because it's failing. +We have changed the id of the form when changing the `.heex` template. +Change the test to the following. -## 12.4 Form feedback -//TODO +```elixir + test "edit-item", %{conn: conn} do + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + {:ok, view, _html} = live(conn, "/") + assert render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) =~ + "
Repo.update() end + @doc """ + Lists all the timer from a given item.id. + + ## Examples + + iex> list_timers(1) + [%Timer{id: 1, start: ~N[2022-07-11 05:15:31], stop: ~N[2022-07-11 05:15:37}] + """ def list_timers(item_id) do from(v in Timer, where: [item_id: ^item_id], order_by: [asc: :id]) |> Repo.all() From 2642334b6626607930f487d13c1f03ed0e9d0e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 09:04:26 +0000 Subject: [PATCH 27/53] fix: Fixing warnings. --- .gitignore | 7 +- lib/app_web/live/app_live.ex | 64 +++++++++---------- .../controllers/tag_controller_test.exs | 2 +- test/app_web/live/app_live_test.exs | 2 - 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 8febc8dc..be491ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -43,8 +43,7 @@ npm-debug.log # VS code elixir_ls folder see https://github.com/JakeBecker/vscode-elixir-ls .elixir_ls -.env - -#elm -elm-stuff/ +# VS code personal run config folder .vscode/ + +.env diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 9f7fdfc0..0b9ac65d 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -129,37 +129,6 @@ defmodule AppWeb.AppLive do {:noreply, assign(socket, editing: nil, editing_timers: [])} end - @nodoc """ - Errors a specific changeset from a list of changesets and returns the updated list of changesets. - You should pass a: - - `timer_changeset_list: list of timer changesets to be updated - - `changeset_to_error`: changeset object that you want to error out - - `changeset_index`: changeset object index inside the list of timer changesets - - `changeset_error_key`: atom key of the changeset object you want to associate the error message - - `changeset_error_message`: the string message to error the changeset key with. - """ - defp error_timer_changeset( - timer_changeset_list, - changeset_to_error, - changeset_index, - changeset_error_key, - changeset_error_message - ) do - # Adding error to changeset - errored_changeset = - Ecto.Changeset.add_error( - changeset_to_error, - changeset_error_key, - changeset_error_message - ) - - {_reply, errored_changeset} = - Ecto.Changeset.apply_action(errored_changeset, :update) - - # Updated list with errored changeset - List.replace_at(timer_changeset_list, changeset_index, errored_changeset) - end - @impl true def handle_event( "update-item-timer", @@ -211,7 +180,7 @@ defmodule AppWeb.AppLive do assign(socket, editing_timers: updated_changeset_timers_list)} end rescue - e -> + _e -> updated_changeset_timers_list = error_timer_changeset( timer_changeset_list, @@ -226,6 +195,37 @@ defmodule AppWeb.AppLive do end end + + # Errors a specific changeset from a list of changesets and returns the updated list of changesets. + # You should pass a: + # - `timer_changeset_list: list of timer changesets to be updated + # - `changeset_to_error`: changeset object that you want to error out + # - `changeset_index`: changeset object index inside the list of timer changesets + # - `changeset_error_key`: atom key of the changeset object you want to associate the error message + # - `changeset_error_message`: the string message to error the changeset key with. + @doc false + defp error_timer_changeset( + timer_changeset_list, + changeset_to_error, + changeset_index, + changeset_error_key, + changeset_error_message + ) do + # Adding error to changeset + errored_changeset = + Ecto.Changeset.add_error( + changeset_to_error, + changeset_error_key, + changeset_error_message + ) + + {_reply, errored_changeset} = + Ecto.Changeset.apply_action(errored_changeset, :update) + + # Updated list with errored changeset + List.replace_at(timer_changeset_list, changeset_index, errored_changeset) + end + @impl true def handle_info(%Broadcast{event: "update", payload: _message}, socket) do person_id = get_person_id(socket.assigns) diff --git a/test/app_web/controllers/tag_controller_test.exs b/test/app_web/controllers/tag_controller_test.exs index 96c5cd0c..b1c92325 100644 --- a/test/app_web/controllers/tag_controller_test.exs +++ b/test/app_web/controllers/tag_controller_test.exs @@ -94,7 +94,7 @@ defmodule AppWeb.TagControllerTest do end defp create_person(_) do - person = Person.create_person(%{"person_id" => 0, "name" => "guest"}) + _person = Person.create_person(%{"person_id" => 0, "name" => "guest"}) person = Person.create_person(%{"person_id" => 1, "name" => "Person1"}) %{person: person} end diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index cc839014..77d7d1b2 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -137,8 +137,6 @@ defmodule AppWeb.AppLiveTest do {:ok, seven_seconds_ago} = NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) - {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) - # Start the timer 7 seconds ago: {:ok, timer} = Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) From d601dc0863feef976285257883a815acca323258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 8 Nov 2022 09:18:48 +0000 Subject: [PATCH 28/53] fix: Format code. --- lib/app_web/live/app_live.ex | 39 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 0b9ac65d..0a41ceae 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -195,7 +195,6 @@ defmodule AppWeb.AppLive do end end - # Errors a specific changeset from a list of changesets and returns the updated list of changesets. # You should pass a: # - `timer_changeset_list: list of timer changesets to be updated @@ -203,28 +202,28 @@ defmodule AppWeb.AppLive do # - `changeset_index`: changeset object index inside the list of timer changesets # - `changeset_error_key`: atom key of the changeset object you want to associate the error message # - `changeset_error_message`: the string message to error the changeset key with. - @doc false - defp error_timer_changeset( - timer_changeset_list, + @doc false + defp error_timer_changeset( + timer_changeset_list, + changeset_to_error, + changeset_index, + changeset_error_key, + changeset_error_message + ) do + # Adding error to changeset + errored_changeset = + Ecto.Changeset.add_error( changeset_to_error, - changeset_index, changeset_error_key, changeset_error_message - ) do - # Adding error to changeset - errored_changeset = - Ecto.Changeset.add_error( - changeset_to_error, - changeset_error_key, - changeset_error_message - ) - - {_reply, errored_changeset} = - Ecto.Changeset.apply_action(errored_changeset, :update) - - # Updated list with errored changeset - List.replace_at(timer_changeset_list, changeset_index, errored_changeset) - end + ) + + {_reply, errored_changeset} = + Ecto.Changeset.apply_action(errored_changeset, :update) + + # Updated list with errored changeset + List.replace_at(timer_changeset_list, changeset_index, errored_changeset) + end @impl true def handle_info(%Broadcast{event: "update", payload: _message}, socket) do From 0f0b8139e67e91a77c1f5cee8bf32e04a71dbe22 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 9 Nov 2022 23:49:02 +0000 Subject: [PATCH 29/53] update table of contents in BUILDIT.md as per https://github.com/dwyl/mvp/pull/196#discussion_r1018188188 --- BUILDIT.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/BUILDIT.md b/BUILDIT.md index c6131715..09ae5a77 100644 --- a/BUILDIT.md +++ b/BUILDIT.md @@ -86,9 +86,19 @@ With that in place, let's get building! - [9. Update the `LiveView` Template](#9-update-the-liveview-template) - [10. Filter Items](#10-filter-items) - [11. Tags](#11-tags) -- [12. Run the _Finished_ MVP App!](#12-run-the-finished-mvp-app) - - [12.1 Run the Tests](#121-run-the-tests) - - [12.2 Run The App](#122-run-the-app) + - [11.1 Migrations](#111-migrations) + - [11.2 Schemas](#112-schemas) + - [11.3 Test tags with Iex](#113-test-tags-with-iex) + - [11.4 Testing Schemas](#114-testing-schemas) + - [11.4 Items-Tags association](#114--items-tags-association) +- [12. Editing timers](#12-editing-timers) + - [12.1 Parsing DateTime strings](#121-parsing-datetime-strings) + - [12.2 Persisting update in database](#122-persisting-update-in-database) + - [12.3 Showing timers in UI](#123-showing-timers-in-ui) + - [12.4 Updating the tests and going back to 100% coverage](#124-updating-the-tests-and-going-back-to-100-coverage) +- [13. Run the _Finished_ MVP App!](#13-run-the-finished-mvp-app) + - [13.1 Run the Tests](#131-run-the-tests) + - [13.2 Run The App](#132-run-the-app) - [Thanks!](#thanks) @@ -3554,11 +3564,11 @@ Change the test to the following. You should now have a function way to change the timers! :wink: -# 12. Run the _Finished_ MVP App! +# 13. Run the _Finished_ MVP App! With all the code saved, let's run the tests one more time. -## 12.1 Run the Tests +## 13.1 Run the Tests In your terminal window, run: @@ -3587,7 +3597,7 @@ COV FILE LINES RELEVANT MISSED All tests pass and we have **`100%` Test Coverage**. This reminds us just how few _relevant_ lines of code there are in the MVP! -## 12.2 Run The App +## 13.2 Run The App In your second terminal tab/window, run: From e125faefbad44ba78c1e993ac172170d828f0d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Thu, 10 Nov 2022 19:27:44 +0000 Subject: [PATCH 30/53] fix: Errors are cleared on another submit. #195. --- lib/app_web/live/app_live.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 0a41ceae..9879213f 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -210,10 +210,12 @@ defmodule AppWeb.AppLive do changeset_error_key, changeset_error_message ) do - # Adding error to changeset + + # Clearing and adding error to changeset + cleared_changeset = Map.put(changeset_to_error, :errors, []) errored_changeset = Ecto.Changeset.add_error( - changeset_to_error, + cleared_changeset, changeset_error_key, changeset_error_message ) From 2f9bc89a18b61e65d359a287b088c2921a6447f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Thu, 10 Nov 2022 21:26:37 +0000 Subject: [PATCH 31/53] feat: Checking if timer being edited overlaps with others #195 --- lib/app_web/live/app_live.ex | 54 ++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 9879213f..a0f0af46 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -145,13 +145,55 @@ defmodule AppWeb.AppLive do changeset_obj = Enum.at(timer_changeset_list, index) try do - start = App.DateTimeParser.parse!(timer_start, "%Y-%m-%dT%H:%M:%S") - stop = App.DateTimeParser.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S") + start = + App.DateTimeParser.parse!(timer_start, "%Y-%m-%dT%H:%M:%S") + |> DateTime.to_naive() - case DateTime.compare(start, stop) do + stop = + App.DateTimeParser.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S") + |> DateTime.to_naive() + + case NaiveDateTime.compare(start, stop) do :lt -> - Timer.update_timer(%{id: id, start: start, stop: stop}) - {:noreply, assign(socket, editing: nil, editing_timers: [])} + # Creates a list of all other timers to check for overlap + other_timers_list = + List.delete_at(socket.assigns.editing_timers, index) + + # Timer overlap verification + try do + for chs <- other_timers_list do + chs_start = chs.data.start + chs_stop = chs.data.stop + + # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) + # so no timer overlaps one another + compareStartAEndB = NaiveDateTime.compare(start, chs_stop) + compareEndAStartB = NaiveDateTime.compare(stop, chs_start) + + if( + (compareStartAEndB == :lt || compareStartAEndB == :eq) && + (compareEndAStartB == :gt || compareEndAStartB == :eq) + ) do + throw(:overlap) + end + end + + # Timer.update_timer(%{id: id, start: start, stop: stop}) + {:noreply, assign(socket, editing: nil, editing_timers: [])} + catch + :overlap -> + updated_changeset_timers_list = + error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other" + ) + + {:noreply, + assign(socket, editing_timers: updated_changeset_timers_list)} + end :eq -> updated_changeset_timers_list = @@ -210,9 +252,9 @@ defmodule AppWeb.AppLive do changeset_error_key, changeset_error_message ) do - # Clearing and adding error to changeset cleared_changeset = Map.put(changeset_to_error, :errors, []) + errored_changeset = Ecto.Changeset.add_error( cleared_changeset, From d2c2b915b3df71efcfdfec435722722f95282e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Thu, 10 Nov 2022 21:35:43 +0000 Subject: [PATCH 32/53] fix: Uncommenting update timer line. --- lib/app_web/live/app_live.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index a0f0af46..004a2e3a 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -178,7 +178,7 @@ defmodule AppWeb.AppLive do end end - # Timer.update_timer(%{id: id, start: start, stop: stop}) + Timer.update_timer(%{id: id, start: start, stop: stop}) {:noreply, assign(socket, editing: nil, editing_timers: [])} catch :overlap -> From ac537518db446a511844a34bbdee69105d84938f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Fri, 11 Nov 2022 13:11:52 +0000 Subject: [PATCH 33/53] fix: Editing timers while running works properly. #195 The timer can be updated while it is running. Validation now works properly. --- lib/app/timer.ex | 23 ++++++- lib/app_web/live/app_live.ex | 102 ++++++++++++++++++++++++---- lib/app_web/live/app_live.html.heex | 1 - 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 31af3ecb..462257d9 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -81,7 +81,7 @@ defmodule App.Timer do end @doc """ - Lists all the timer from a given item.id. + Lists all the timers from a given item.id. ## Examples @@ -93,6 +93,27 @@ defmodule App.Timer do |> Repo.all() end + @doc """ + Lists all the timers changesets from a given item.id. + This is useful for form validation, as it returns the timers in a changeset form, in which you can add errors. + + ## Examples + + iex> list_timers(1) + [ #Ecto.Changeset, valid?: true> ] + """ + def list_timers_changesets(item_id) do + from(v in Timer, where: [item_id: ^item_id], order_by: [asc: :id]) + |> Repo.all() + |> Enum.map(fn t -> + Timer.changeset(t, %{ + id: t.id, + start: t.start, + stop: t.stop, + item_id: t.item_id + }) end) + end + @doc """ `stop_timer_for_item_id/1` stops a timer for the given item_id if there is one. Fails silently if there is no timer for the given item_id. diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 004a2e3a..35728ae2 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -77,7 +77,7 @@ defmodule AppWeb.AppLive do start: NaiveDateTime.utc_now() }) - AppWeb.Endpoint.broadcast(@topic, "update", :start) + AppWeb.Endpoint.broadcast(@topic, "update", {:start, item.id}) {:noreply, socket} end @@ -86,7 +86,7 @@ defmodule AppWeb.AppLive do timer_id = Map.get(data, "timerid") {:ok, _timer} = Timer.stop(%{id: timer_id}) - AppWeb.Endpoint.broadcast(@topic, "update", :stop) + AppWeb.Endpoint.broadcast(@topic, "update", {:stop, Map.get(data, "id")}) {:noreply, socket} end @@ -94,17 +94,7 @@ defmodule AppWeb.AppLive do def handle_event("edit-item", data, socket) do item_id = String.to_integer(data["id"]) - timers_list = Timer.list_timers(item_id) - - timers_list_changeset = - Enum.map(timers_list, fn t -> - Timer.changeset(t, %{ - id: t.id, - start: t.start, - stop: t.stop, - item_id: t.item_id - }) - end) + timers_list_changeset = Timer.list_timers_changesets(item_id) {:noreply, assign(socket, editing: item_id, editing_timers: timers_list_changeset)} @@ -130,6 +120,66 @@ defmodule AppWeb.AppLive do end @impl true + def handle_event( + "update-item-timer", + %{ + "timer_id" => id, + "index" => index, + "timer_start" => timer_start, + "timer_stop" => timer_stop + }, + socket + ) when timer_stop == "" do + + timer_changeset_list = socket.assigns.editing_timers + index = String.to_integer(index) + changeset_obj = Enum.at(timer_changeset_list, index) + + try do + start = + App.DateTimeParser.parse!(timer_start, "%Y-%m-%dT%H:%M:%S") + |> DateTime.to_naive() + + other_timers_list = + List.delete_at(socket.assigns.editing_timers, index) + + max_end = other_timers_list |> Enum.map(fn chs -> chs.data.stop end) |> Enum.max() + + case NaiveDateTime.compare(start, max_end) do + :gt -> + Timer.update_timer(%{id: id, start: start, stop: nil}) + {:noreply, assign(socket, editing: nil, editing_timers: [])} + _ -> + updated_changeset_timers_list = + error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "When editing an ongoing timer, make sure it's after all the others." + ) + + {:noreply, + assign(socket, editing_timers: updated_changeset_timers_list)} + + end + + rescue + _e -> + updated_changeset_timers_list = + error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Date format invalid on either start or stop." + ) + + {:noreply, + assign(socket, editing_timers: updated_changeset_timers_list)} + end + end + def handle_event( "update-item-timer", %{ @@ -270,11 +320,33 @@ defmodule AppWeb.AppLive do end @impl true - def handle_info(%Broadcast{event: "update", payload: _message}, socket) do + def handle_info(%Broadcast{event: "update", payload: payload}, socket) do + person_id = get_person_id(socket.assigns) items = Item.items_with_timers(person_id) - {:noreply, assign(socket, items: items)} + isEditingItem = socket.assigns.editing + + # If the item is being edited, we update the timer list of the item being edited. + if isEditingItem do + case payload do + {:start, item_id} -> + timers_list_changeset = Timer.list_timers_changesets(item_id) + {:noreply, assign(socket, items: items, editing: item_id, editing_timers: timers_list_changeset)} + + + {:stop, item_id} -> + timers_list_changeset = Timer.list_timers_changesets(item_id) + {:noreply, assign(socket, items: items, editing: item_id, editing_timers: timers_list_changeset)} + + _ -> + {:noreply, assign(socket, items: items)} + end + + # If not, just update the item list. + else + {:noreply, assign(socket, items: items)} + end end # only show certain UI elements (buttons) if there are items: diff --git a/lib/app_web/live/app_live.html.heex b/lib/app_web/live/app_live.html.heex index 316ec07e..db127c54 100644 --- a/lib/app_web/live/app_live.html.heex +++ b/lib/app_web/live/app_live.html.heex @@ -256,7 +256,6 @@ From 1bcdacaf1f0642c9e9d841a08850996b51bbc8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Fri, 11 Nov 2022 17:16:49 +0000 Subject: [PATCH 34/53] feat: Adding tests to test behaviour when editing timers. #195 --- lib/app/timer.ex | 15 +- test/app_web/live/app_live_test.exs | 217 +++++++++++++++++++++++++++- 2 files changed, 217 insertions(+), 15 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 462257d9..c6dc3788 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -80,26 +80,13 @@ defmodule App.Timer do |> Repo.update() end - @doc """ - Lists all the timers from a given item.id. - - ## Examples - - iex> list_timers(1) - [%Timer{id: 1, start: ~N[2022-07-11 05:15:31], stop: ~N[2022-07-11 05:15:37}] - """ - def list_timers(item_id) do - from(v in Timer, where: [item_id: ^item_id], order_by: [asc: :id]) - |> Repo.all() - end - @doc """ Lists all the timers changesets from a given item.id. This is useful for form validation, as it returns the timers in a changeset form, in which you can add errors. ## Examples - iex> list_timers(1) + iex> list_timers_changesets(1) [ #Ecto.Changeset, valid?: true> ] """ def list_timers_changesets(item_id) do diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index 77d7d1b2..5286a23b 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -98,6 +98,72 @@ defmodule AppWeb.AppLiveTest do assert render(view) =~ item.text end + test "handle_info/2 update with editing open (start)", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + {:ok, item} = + Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) + + {:ok, now} = + NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + now_string = NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) + |> List.to_string() + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + render_click(view, "start", %{"id" => Integer.to_string(item.id)}) + + # The editing panel is open and showing the newly created timer on the 'Start' text input field + assert render(view) =~ now_string + end + + test "handle_info/2 update with editing open (stop)", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + {:ok, item} = + Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) + + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + render_click(view, "start", %{"id" => Integer.to_string(item.id)}) + render_click(view, "stop", %{"timerid" => timer.id, "id" => item.id}) + + num_timers_rendered = (render(view) |> String.split("Update") |> length()) - 1 + + # Checking if two timers were rendered + assert num_timers_rendered = 2 + end + + test "handle_info/2 update with editing open (delete)", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + {:ok, item} = + Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + send(view.pid, %Broadcast{ + event: "update", + payload: :delete + }) + + assert render(view) =~ item.text + end + test "edit-item", %{conn: conn} do {:ok, item} = Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) @@ -192,6 +258,155 @@ defmodule AppWeb.AppLiveTest do }) =~ "Date format invalid on either start or stop." end + test "update timer timer with ongoing timer ", %{conn: conn} do + + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + {:ok, now} = + NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + {:ok, four_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -4)) + + {:ok, ten_seconds_after} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), 10)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + # Start a second timer + {:ok, timer2} = + Timer.start(%{item_id: item.id, person_id: 1, start: now}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + {:ok, view, _html} = live(conn, "/") + + # Update fails because of overlap timer ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + four_seconds_ago_string = NaiveDateTime.truncate(four_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) + |> List.to_string() + + + now_string = NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) + |> List.to_string() + + error_view = render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => four_seconds_ago_string, + "timer_stop" => "" + }) + assert error_view =~ "When editing an ongoing timer" + + # Update fails because of format ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + error_format_view = render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => "invalidformat", + "timer_stop" => "" + }) + assert error_format_view =~ "Date format invalid on either start or stop." + + # Update successful ----------- + ten_seconds_after_string = NaiveDateTime.truncate(ten_seconds_after, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) + |> List.to_string() + ten_seconds_after_datetime = NaiveDateTime.truncate(ten_seconds_after, :second) + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + view = assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => ten_seconds_after_string, + "timer_stop" => "" + }) + + updated_timer2 = Timer.get_timer!(timer2.id) + + assert updated_timer2.start == ten_seconds_after_datetime + end + + test "timer overlap error when updating timer", %{conn: conn} do + + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + {:ok, now} = + NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + {:ok, four_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -4)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + # Start a second timer + {:ok, timer2} = + Timer.start(%{item_id: item.id, person_id: 1, start: now}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + {:ok, view, _html} = live(conn, "/") + + # Update fails because of overlap ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + four_seconds_ago_string = NaiveDateTime.truncate(four_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) + |> List.to_string() + + + now_string = NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) + |> List.to_string() + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 0, + "timer_start" => four_seconds_ago_string, + "timer_stop" => now_string + }) =~ "This timer interval overlaps with other timers." + end + test "timer_text(start, stop)" do timer = %{ start: ~N[2022-07-17 09:01:42.000000], @@ -319,7 +534,7 @@ defmodule AppWeb.AppLiveTest do test "test login link redirect to auth.dwyl.com", %{conn: conn} do conn = get(conn, "/login") - assert redirected_to(conn, 302) =~ "auth.dwyl.com" + assert redirected_to(conn, 302) =~ "https://dwylauth.herokuapp.com?referer=https://www.example.com/&auth_client_id=88SwQGzxQEvo6S9Pu7FZGp9btNo52rVkwtrhyub9i6K6UxVqho9A" end test "tags_to_string/1" do From 76c0a50266ea9256fb1f03ab584653be0528d07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Fri, 11 Nov 2022 17:18:51 +0000 Subject: [PATCH 35/53] fix: Formatting files. #195 --- lib/app/timer.ex | 3 +- lib/app_web/live/app_live.ex | 69 +++++----- test/app_web/live/app_live_test.exs | 191 +++++++++++++++++----------- 3 files changed, 157 insertions(+), 106 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index c6dc3788..a4014d4c 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -98,7 +98,8 @@ defmodule App.Timer do start: t.start, stop: t.stop, item_id: t.item_id - }) end) + }) + end) end @doc """ diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 35728ae2..be81fe66 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -121,16 +121,16 @@ defmodule AppWeb.AppLive do @impl true def handle_event( - "update-item-timer", - %{ - "timer_id" => id, - "index" => index, - "timer_start" => timer_start, - "timer_stop" => timer_stop - }, - socket - ) when timer_stop == "" do - + "update-item-timer", + %{ + "timer_id" => id, + "index" => index, + "timer_start" => timer_start, + "timer_stop" => timer_stop + }, + socket + ) + when timer_stop == "" do timer_changeset_list = socket.assigns.editing_timers index = String.to_integer(index) changeset_obj = Enum.at(timer_changeset_list, index) @@ -140,15 +140,16 @@ defmodule AppWeb.AppLive do App.DateTimeParser.parse!(timer_start, "%Y-%m-%dT%H:%M:%S") |> DateTime.to_naive() - other_timers_list = - List.delete_at(socket.assigns.editing_timers, index) + other_timers_list = List.delete_at(socket.assigns.editing_timers, index) - max_end = other_timers_list |> Enum.map(fn chs -> chs.data.stop end) |> Enum.max() + max_end = + other_timers_list |> Enum.map(fn chs -> chs.data.stop end) |> Enum.max() case NaiveDateTime.compare(start, max_end) do :gt -> Timer.update_timer(%{id: id, start: start, stop: nil}) {:noreply, assign(socket, editing: nil, editing_timers: [])} + _ -> updated_changeset_timers_list = error_timer_changeset( @@ -161,22 +162,20 @@ defmodule AppWeb.AppLive do {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} - end - rescue - _e -> - updated_changeset_timers_list = - error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Date format invalid on either start or stop." - ) + _e -> + updated_changeset_timers_list = + error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Date format invalid on either start or stop." + ) - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} + {:noreply, + assign(socket, editing_timers: updated_changeset_timers_list)} end end @@ -321,7 +320,6 @@ defmodule AppWeb.AppLive do @impl true def handle_info(%Broadcast{event: "update", payload: payload}, socket) do - person_id = get_person_id(socket.assigns) items = Item.items_with_timers(person_id) @@ -332,18 +330,29 @@ defmodule AppWeb.AppLive do case payload do {:start, item_id} -> timers_list_changeset = Timer.list_timers_changesets(item_id) - {:noreply, assign(socket, items: items, editing: item_id, editing_timers: timers_list_changeset)} + {:noreply, + assign(socket, + items: items, + editing: item_id, + editing_timers: timers_list_changeset + )} {:stop, item_id} -> timers_list_changeset = Timer.list_timers_changesets(item_id) - {:noreply, assign(socket, items: items, editing: item_id, editing_timers: timers_list_changeset)} + + {:noreply, + assign(socket, + items: items, + editing: item_id, + editing_timers: timers_list_changeset + )} _ -> {:noreply, assign(socket, items: items)} end - # If not, just update the item list. + # If not, just update the item list. else {:noreply, assign(socket, items: items)} end diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index 5286a23b..b0e49db2 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -104,14 +104,20 @@ defmodule AppWeb.AppLiveTest do {:ok, item} = Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) - {:ok, now} = - NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) - now_string = NaiveDateTime.truncate(now, :second) + now_string = + NaiveDateTime.truncate(now, :second) |> NaiveDateTime.to_string() |> String.graphemes() |> Enum.with_index() - |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) |> List.to_string() render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) @@ -127,7 +133,6 @@ defmodule AppWeb.AppLiveTest do {:ok, item} = Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) - {:ok, seven_seconds_ago} = NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) @@ -142,7 +147,8 @@ defmodule AppWeb.AppLiveTest do render_click(view, "start", %{"id" => Integer.to_string(item.id)}) render_click(view, "stop", %{"timerid" => timer.id, "id" => item.id}) - num_timers_rendered = (render(view) |> String.split("Update") |> length()) - 1 + num_timers_rendered = + (render(view) |> String.split("Update") |> length()) - 1 # Checking if two timers were rendered assert num_timers_rendered = 2 @@ -259,15 +265,13 @@ defmodule AppWeb.AppLiveTest do end test "update timer timer with ongoing timer ", %{conn: conn} do - {:ok, item} = Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) {:ok, seven_seconds_ago} = NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) - {:ok, now} = - NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) {:ok, four_seconds_ago} = NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -4)) @@ -283,8 +287,7 @@ defmodule AppWeb.AppLiveTest do Timer.stop_timer_for_item_id(item.id) # Start a second timer - {:ok, timer2} = - Timer.start(%{item_id: item.id, person_id: 1, start: now}) + {:ok, timer2} = Timer.start(%{item_id: item.id, person_id: 1, start: now}) # Stop the timer based on its item_id Timer.stop_timer_for_item_id(item.id) @@ -294,57 +297,84 @@ defmodule AppWeb.AppLiveTest do # Update fails because of overlap timer ----------- render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) - four_seconds_ago_string = NaiveDateTime.truncate(four_seconds_ago, :second) - |> NaiveDateTime.to_string() - |> String.graphemes() - |> Enum.with_index() - |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) - |> List.to_string() - - - now_string = NaiveDateTime.truncate(now, :second) - |> NaiveDateTime.to_string() - |> String.graphemes() - |> Enum.with_index() - |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) - |> List.to_string() - - error_view = render_submit(view, "update-item-timer", %{ - "timer_id" => timer2.id, - "index" => 1, - "timer_start" => four_seconds_ago_string, - "timer_stop" => "" - }) + four_seconds_ago_string = + NaiveDateTime.truncate(four_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + now_string = + NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + error_view = + render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => four_seconds_ago_string, + "timer_stop" => "" + }) + assert error_view =~ "When editing an ongoing timer" # Update fails because of format ----------- render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) - error_format_view = render_submit(view, "update-item-timer", %{ - "timer_id" => timer2.id, - "index" => 1, - "timer_start" => "invalidformat", - "timer_stop" => "" - }) + error_format_view = + render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => "invalidformat", + "timer_stop" => "" + }) + assert error_format_view =~ "Date format invalid on either start or stop." # Update successful ----------- - ten_seconds_after_string = NaiveDateTime.truncate(ten_seconds_after, :second) - |> NaiveDateTime.to_string() - |> String.graphemes() - |> Enum.with_index() - |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) - |> List.to_string() - ten_seconds_after_datetime = NaiveDateTime.truncate(ten_seconds_after, :second) + ten_seconds_after_string = + NaiveDateTime.truncate(ten_seconds_after, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + ten_seconds_after_datetime = + NaiveDateTime.truncate(ten_seconds_after, :second) render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) - view = assert render_submit(view, "update-item-timer", %{ - "timer_id" => timer2.id, - "index" => 1, - "timer_start" => ten_seconds_after_string, - "timer_stop" => "" - }) + view = + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => ten_seconds_after_string, + "timer_stop" => "" + }) updated_timer2 = Timer.get_timer!(timer2.id) @@ -352,15 +382,13 @@ defmodule AppWeb.AppLiveTest do end test "timer overlap error when updating timer", %{conn: conn} do - {:ok, item} = Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) {:ok, seven_seconds_ago} = NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) - {:ok, now} = - NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) {:ok, four_seconds_ago} = NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -4)) @@ -373,8 +401,7 @@ defmodule AppWeb.AppLiveTest do Timer.stop_timer_for_item_id(item.id) # Start a second timer - {:ok, timer2} = - Timer.start(%{item_id: item.id, person_id: 1, start: now}) + {:ok, timer2} = Timer.start(%{item_id: item.id, person_id: 1, start: now}) # Stop the timer based on its item_id Timer.stop_timer_for_item_id(item.id) @@ -384,27 +411,40 @@ defmodule AppWeb.AppLiveTest do # Update fails because of overlap ----------- render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) - four_seconds_ago_string = NaiveDateTime.truncate(four_seconds_ago, :second) - |> NaiveDateTime.to_string() - |> String.graphemes() - |> Enum.with_index() - |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) - |> List.to_string() - + four_seconds_ago_string = + NaiveDateTime.truncate(four_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() - now_string = NaiveDateTime.truncate(now, :second) - |> NaiveDateTime.to_string() - |> String.graphemes() - |> Enum.with_index() - |> Enum.map(fn {value, index} -> if index == 10 do "T" else value end end) - |> List.to_string() + now_string = + NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() assert render_submit(view, "update-item-timer", %{ - "timer_id" => timer2.id, - "index" => 0, - "timer_start" => four_seconds_ago_string, - "timer_stop" => now_string - }) =~ "This timer interval overlaps with other timers." + "timer_id" => timer2.id, + "index" => 0, + "timer_start" => four_seconds_ago_string, + "timer_stop" => now_string + }) =~ "This timer interval overlaps with other timers." end test "timer_text(start, stop)" do @@ -534,7 +574,8 @@ defmodule AppWeb.AppLiveTest do test "test login link redirect to auth.dwyl.com", %{conn: conn} do conn = get(conn, "/login") - assert redirected_to(conn, 302) =~ "https://dwylauth.herokuapp.com?referer=https://www.example.com/&auth_client_id=88SwQGzxQEvo6S9Pu7FZGp9btNo52rVkwtrhyub9i6K6UxVqho9A" + assert redirected_to(conn, 302) =~ + "https://dwylauth.herokuapp.com?referer=https://www.example.com/&auth_client_id=88SwQGzxQEvo6S9Pu7FZGp9btNo52rVkwtrhyub9i6K6UxVqho9A" end test "tags_to_string/1" do From b2171d75dbc95d7c1221f3ae30d5775df2f0839d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Fri, 11 Nov 2022 17:20:47 +0000 Subject: [PATCH 36/53] fix: Fixing auth test. #195 --- test/app_web/live/app_live_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index b0e49db2..d853f060 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -574,8 +574,8 @@ defmodule AppWeb.AppLiveTest do test "test login link redirect to auth.dwyl.com", %{conn: conn} do conn = get(conn, "/login") - assert redirected_to(conn, 302) =~ - "https://dwylauth.herokuapp.com?referer=https://www.example.com/&auth_client_id=88SwQGzxQEvo6S9Pu7FZGp9btNo52rVkwtrhyub9i6K6UxVqho9A" + assert redirected_to(conn, 302) =~ "auth.dwyl.com" + end test "tags_to_string/1" do From 356d69a2cf5a9e5459aef10089af6876628851fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Fri, 11 Nov 2022 17:22:16 +0000 Subject: [PATCH 37/53] fix: Format test file. --- test/app_web/live/app_live_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index d853f060..292bf76a 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -575,7 +575,6 @@ defmodule AppWeb.AppLiveTest do conn = get(conn, "/login") assert redirected_to(conn, 302) =~ "auth.dwyl.com" - end test "tags_to_string/1" do From 8d7a449d899f2cb29e6601956831d4635777366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 11:04:27 +0000 Subject: [PATCH 38/53] fix: Deleting parser in favour of using Timex. --- lib/app/datetime_parser.ex | 196 ------------------------------ lib/app_web/live/app_live.ex | 10 +- mix.exs | 1 + mix.lock | 4 + test/app/datetime_parser_test.exs | 81 ------------ 5 files changed, 9 insertions(+), 283 deletions(-) delete mode 100644 lib/app/datetime_parser.ex delete mode 100644 test/app/datetime_parser_test.exs diff --git a/lib/app/datetime_parser.ex b/lib/app/datetime_parser.ex deleted file mode 100644 index 81cf804e..00000000 --- a/lib/app/datetime_parser.ex +++ /dev/null @@ -1,196 +0,0 @@ -# Full credit of this module goes to https://dev.to/onpointvn/build-your-own-date-time-parser-in-elixir-50be -# Do check the gist -> https://gist.github.com/bluzky/62a20cdb57b17f47c67261c10aa3da8b -defmodule App.DateTimeParser do - @mapping %{ - "H" => "(?\\d{2})", - "I" => "(?\\d{2})", - "M" => "(?\\d{2})", - "S" => "(?\\d{2})", - "d" => "(?\\d{2})", - "m" => "(?\\d{2})", - "y" => "(?\\d{2})", - "Y" => "(?-?\\d{4})", - "z" => "(?[+-]?\\d{4})", - "Z" => "(?[a-zA-Z_\/]+)", - "p" => "(?

PM|AM)", - "P" => "(?

pm|am)", - "%" => "%" - } - - @doc """ - Parse string to datetime struct - **Example** - parse("2021-20-10", "%Y-%M-%d") - Support format - | format | description| value example | - | -- | -- | -- | - | H | 24 hour | 00 - 23 | - | I | 12 hour | 00 - 12 | - | M | minute| 00 - 59 | - | S | second | 00 - 59 | - | d | day | 01 - 31 | - | m | month | 01 -12 | - | y | 2 digits year | 00 - 99 | - | Y | 4 digits year | | - | z | timezone offset | +0100, -0330 | - | Z | timezone name | UTC+7, Asia/Ho_Chi_Minh | - | p | PM or AM | | - | P | pm or am | | - """ - def parse!(dt_string, format \\ "%Y-%m-%dT%H:%M:%SZ") do - case parse(dt_string, format) do - {:ok, dt} -> - dt - - {:error, message} -> - raise "Parse string #{dt_string} with error: #{message}" - end - end - - @doc """ - Parses the string according to the format. Pipes through regex compilation, casts each part of the string to a named regex capture and tries to convert to datetime. - """ - def parse(dt_string, format \\ "%Y-%m-%dT%H:%M:%SZ") do - format - |> build_regex - |> Regex.named_captures(dt_string) - |> cast_data - |> to_datetime - end - - @doc """ - Builds the regex expression to later be captured (have named key-value captures).any() - This uses the @mapping structure to name specific parts of the entered string to convert to datetime. - """ - def build_regex(format) do - keys = Map.keys(@mapping) |> Enum.join("") - - Regex.compile!("([^%]*)%([#{keys}])([^%]*)") - |> Regex.scan(format) - |> Enum.map(fn [_, s1, key, s2] -> - [s1, Map.get(@mapping, key), s2] - end) - |> to_string() - |> Regex.compile!() - end - - @default_value %{ - day: 1, - month: 1, - year: 0, - hour: 0, - minute: 0, - second: 0, - utc_offset: 0, - tz_name: "UTC", - shift: "AM" - } - def cast_data(nil), do: {:error, "invalid datetime"} - - @doc """ - Casts each capture of the regex to appropriated format (compatible with DateTime struct) - """ - def cast_data(captures) do - captures - |> Enum.reduce_while([], fn {part, value}, acc -> - {:ok, data} = cast(part, value) - {:cont, [data | acc]} - end) - |> Enum.into(@default_value) - end - - @value_rages %{ - "hour" => [0, 23], - "hour12" => [0, 12], - "minute" => [0, 59], - "second" => [0, 59], - "day" => [0, 31], - "month" => [1, 12], - "year2" => [0, 99] - } - - defp cast("P", value) do - cast("p", String.upcase(value)) - end - - defp cast("p", value) do - {:ok, {:shift, value}} - end - - defp cast("tz", value) do - {hour, minute} = String.split_at(value, 3) - - with {:ok, {_, hour}} <- cast("offset_h", hour), - {:ok, {_, minute}} <- cast("offset_m", minute) do - sign = div(hour, abs(hour)) - {:ok, {:utc_offset, sign * (abs(hour) * 3600 + minute * 60)}} - end - end - - defp cast("tz_name", value) do - {:ok, {:tz_name, value}} - end - - defp cast(part, value) do - value = String.to_integer(value) - - valid = - case Map.get(@value_rages, part) do - [min, max] -> - value >= min and value <= max - - _ -> - true - end - - if valid do - {:ok, {String.to_atom(part), value}} - end - end - - defp to_datetime({:error, _} = error), do: error - - defp to_datetime(%{year2: value} = data) do - current_year = DateTime.utc_now() |> Map.get(:year) - year = div(current_year, 100) * 100 + value - - data - |> Map.put(:year, year) - |> Map.delete(:year2) - |> to_datetime() - end - - defp to_datetime(%{hour12: hour} = data) do - # 12AM is not valid - - if hour == 12 and data.shift == "AM" do - {:error, "12AM is invalid value"} - else - hour = - cond do - hour == 12 and data.shift == "PM" -> hour - data.shift == "AM" -> hour - data.shift == "PM" -> hour + 12 - end - - data - |> Map.put(:hour, hour) - |> Map.delete(:hour12) - |> to_datetime() - end - end - - defp to_datetime(data) do - with {:ok, date} <- Date.new(data.year, data.month, data.day), - {:ok, time} <- Time.new(data.hour, data.minute, data.second), - {:ok, datetime} <- DateTime.new(date, time) do - datetime = DateTime.add(datetime, -data.utc_offset, :second) - - if data.tz_name != "UTC" do - {:error, "Only UTC timezone is available"} - else - {:ok, datetime} - end - end - end -end diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index be81fe66..46084462 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -2,6 +2,7 @@ defmodule AppWeb.AppLive do require Logger use AppWeb, :live_view + use Timex alias App.{Item, Timer} # run authentication on mount on_mount AppWeb.AuthController @@ -137,8 +138,7 @@ defmodule AppWeb.AppLive do try do start = - App.DateTimeParser.parse!(timer_start, "%Y-%m-%dT%H:%M:%S") - |> DateTime.to_naive() + Timex.parse!(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) other_timers_list = List.delete_at(socket.assigns.editing_timers, index) @@ -195,12 +195,10 @@ defmodule AppWeb.AppLive do try do start = - App.DateTimeParser.parse!(timer_start, "%Y-%m-%dT%H:%M:%S") - |> DateTime.to_naive() + Timex.parse!(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) stop = - App.DateTimeParser.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S") - |> DateTime.to_naive() + Timex.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S", :strftime) case NaiveDateTime.compare(start, stop) do :lt -> diff --git a/mix.exs b/mix.exs index da1cbcf9..2932e98f 100644 --- a/mix.exs +++ b/mix.exs @@ -54,6 +54,7 @@ defmodule App.MixProject do {:telemetry_poller, "~> 1.0"}, {:jason, "~> 1.2"}, {:plug_cowboy, "~> 2.5"}, + {:timex, "~> 3.7"}, # Check/get Environment Variables: https://github.com/dwyl/envar {:envar, "~> 1.0.8"}, diff --git a/mix.lock b/mix.lock index 0715f660..12548ae1 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,7 @@ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, @@ -24,6 +25,7 @@ "fields": {:hex, :fields, "2.9.1", "af8ce8e90e0e33df3ca173adec0839f34778c229211e35f5258971fe57ee40ff", [:mix], [{:argon2_elixir, "~> 3.0.0", [hex: :argon2_elixir, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8", [hex: :ecto, repo: "hexpm", optional: false]}, {:envar, "~> 1.0.8", [hex: :envar, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4.2", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}], "hexpm", "caa388160938fc5180c32bec66514c87ebae3a4029c9371f42e9ccb0093ad7f5"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"}, + "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "heroicons": {:hex, :heroicons, "0.5.1", "cca0dcca07af5f74d8a7d111e40418d3615d65e6773c0ea10e20cef070fd30aa", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "4b096d0a1d50e9054df9b12cc637c9f65c3972ff086791d3f2d1846f0653117e"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, @@ -62,6 +64,8 @@ "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"}, + "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "useful": {:hex, :useful, "1.0.8", "795a5bf94567e4a1b374621577acabc80ea80634b634095237f98e40e64e9d24", [:mix], [], "hexpm", "947ae0ba2b3c06bcfd8994e95e29f4cc13287aab81b481ae6abb9077fc9c1ad5"}, } diff --git a/test/app/datetime_parser_test.exs b/test/app/datetime_parser_test.exs deleted file mode 100644 index af72259c..00000000 --- a/test/app/datetime_parser_test.exs +++ /dev/null @@ -1,81 +0,0 @@ -defmodule App.DateTimeParserTest do - use App.DataCase - alias App.DateTimeParser - - test "valid parse of valid datetime" do - parsed_time = - DateTimeParser.parse!("2022-10-27 14:47:56", "%Y-%m-%d %H:%M:%S") - - {:ok, expected_datetime, 0} = DateTime.from_iso8601("2022-10-27T14:47:56Z") - - assert parsed_time == expected_datetime - end - - test "valid parse of valid date with %Y-%m-%d format" do - parsed_time = DateTimeParser.parse!("2022-10-27", "%Y-%m-%d") - {:ok, expected_datetime, 0} = DateTime.from_iso8601("2022-10-27T00:00:00Z") - - assert parsed_time == expected_datetime - end - - test "non-compatible regex when parsing" do - assert_raise Regex.CompileError, fn -> - DateTimeParser.parse!("2022-10-27 14:47:56", "%Y-%Y-%Y") - end - end - - test "invalid datetime format" do - assert_raise RuntimeError, fn -> - DateTimeParser.parse!("2022-102-2752 1423:4127:56", "%Y-%m-%d %H:%M:%S") - end - end - - test "valid timezone offset (with tz)" do - parsed_date = - DateTimeParser.parse!("2022-10-27T00:00:00Z+0230", "%Y-%m-%dT%H:%M:%SZ%z") - - {:ok, expected_datetime, 9000} = - DateTime.from_iso8601("2022-10-27T00:00:00+02:30") - - assert parsed_date == expected_datetime - end - - test "valid timezone offset (with UTC)" do - parsed_date = - DateTimeParser.parse!( - "2022-10-27T00:00:00ZUTC+0230", - "%Y-%m-%dT%H:%M:%SZ%Z" - ) - - assert parsed_date == ~U[2022-10-27 00:00:00Z] - end - - test "invalid timezone name" do - assert_raise RuntimeError, fn -> - DateTimeParser.parse!( - "2022-10-27T00:00:00ZEtc+0230", - "%Y-%m-%dT%H:%M:%SZ%Z" - ) - end - end - - test "valid datetime with PM/AM" do - date_under = DateTimeParser.parse!("2022-10-27 06:02pm", "%Y-%m-%d %H:%M%P") - date_sup = DateTimeParser.parse!("2022-10-27 06:02PM", "%Y-%m-%d %H:%M%p") - - assert date_under == date_sup - end - - test "valid datetime with PM/AM with two digits" do - parsed_datetime = - DateTimeParser.parse!("2022-10-27 06:02pm", "%Y-%m-%d %I:%M%P") - - assert parsed_datetime == ~U[2022-10-27 18:02:00Z] - end - - test "valid datetime with two-digit year" do - parsed_date = DateTimeParser.parse!("10-10-27", "%y-%m-%d") - - assert parsed_date == ~U[2010-10-27 00:00:00Z] - end -end From be0ba435321c253d2c54eb7ee495b38cb45ff26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 11:34:56 +0000 Subject: [PATCH 39/53] fix: Refactoring erroring timer changeset to Timer file. --- lib/app/timer.ex | 36 ++++++++++++++++++++++ lib/app_web/live/app_live.ex | 60 +++++++++++------------------------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index a4014d4c..017e43f9 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -102,6 +102,42 @@ defmodule App.Timer do end) end + @doc """ + Errors a specific changeset from a list of changesets and returns the updated list of changesets. + You should pass a: + - `timer_changeset_list: list of timer changesets to be updated + - `changeset_to_error`: changeset object that you want to error out + - `changeset_index`: changeset object index inside the list of timer changesets (faster lookup) + - `error_key`: atom key of the changeset object you want to associate the error message to + - `error_message`: the string message to error the changeset key with. + - `action`: action atom to apply to errored changeset. + """ + def error_timer_changeset( + timer_changeset_list, + changeset_to_error, + changeset_index, + error_key, + error_message, + action + ) do + + # Clearing and adding error to changeset + cleared_changeset = Map.put(changeset_to_error, :errors, []) + + errored_changeset = + Ecto.Changeset.add_error( + cleared_changeset, + error_key, + error_message + ) + + {_reply, errored_changeset} = + Ecto.Changeset.apply_action(errored_changeset, action) + + # Updated list with errored changeset + List.replace_at(timer_changeset_list, changeset_index, errored_changeset) + end + @doc """ `stop_timer_for_item_id/1` stops a timer for the given item_id if there is one. Fails silently if there is no timer for the given item_id. diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 46084462..458f6a22 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -152,12 +152,13 @@ defmodule AppWeb.AppLive do _ -> updated_changeset_timers_list = - error_timer_changeset( + Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "When editing an ongoing timer, make sure it's after all the others." + "When editing an ongoing timer, make sure it's after all the others.", + :update ) {:noreply, @@ -166,12 +167,13 @@ defmodule AppWeb.AppLive do rescue _e -> updated_changeset_timers_list = - error_timer_changeset( + Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "Date format invalid on either start or stop." + "Date format invalid on either start or stop.", + :update ) {:noreply, @@ -230,12 +232,13 @@ defmodule AppWeb.AppLive do catch :overlap -> updated_changeset_timers_list = - error_timer_changeset( + Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other" + "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other", + :update ) {:noreply, @@ -244,12 +247,13 @@ defmodule AppWeb.AppLive do :eq -> updated_changeset_timers_list = - error_timer_changeset( + Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "Start or stop are equal." + "Start or stop are equal.", + :update ) {:noreply, @@ -257,12 +261,13 @@ defmodule AppWeb.AppLive do :gt -> updated_changeset_timers_list = - error_timer_changeset( + Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "Start is newer that stop." + "Start is newer that stop.", + :update ) {:noreply, @@ -271,12 +276,13 @@ defmodule AppWeb.AppLive do rescue _e -> updated_changeset_timers_list = - error_timer_changeset( + Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "Date format invalid on either start or stop." + "Date format invalid on either start or stop.", + :update ) {:noreply, @@ -284,37 +290,7 @@ defmodule AppWeb.AppLive do end end - # Errors a specific changeset from a list of changesets and returns the updated list of changesets. - # You should pass a: - # - `timer_changeset_list: list of timer changesets to be updated - # - `changeset_to_error`: changeset object that you want to error out - # - `changeset_index`: changeset object index inside the list of timer changesets - # - `changeset_error_key`: atom key of the changeset object you want to associate the error message - # - `changeset_error_message`: the string message to error the changeset key with. - @doc false - defp error_timer_changeset( - timer_changeset_list, - changeset_to_error, - changeset_index, - changeset_error_key, - changeset_error_message - ) do - # Clearing and adding error to changeset - cleared_changeset = Map.put(changeset_to_error, :errors, []) - - errored_changeset = - Ecto.Changeset.add_error( - cleared_changeset, - changeset_error_key, - changeset_error_message - ) - - {_reply, errored_changeset} = - Ecto.Changeset.apply_action(errored_changeset, :update) - # Updated list with errored changeset - List.replace_at(timer_changeset_list, changeset_index, errored_changeset) - end @impl true def handle_info(%Broadcast{event: "update", payload: payload}, socket) do From 5f70ee99380255795fff69ff1dfae04e8606b87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 12:51:51 +0000 Subject: [PATCH 40/53] fix: Refactoring changeset code to timer.ex. --- lib/app/timer.ex | 119 ++++++++++++++++++++++++++++++++--- lib/app_web/live/app_live.ex | 100 +++-------------------------- 2 files changed, 118 insertions(+), 101 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 017e43f9..4379220f 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -80,6 +80,109 @@ defmodule App.Timer do |> Repo.update() end + def update_timer_inside_changeset_list( + timer_id, + timer_start, + timer_stop, + index, + timer_changeset_list + ) do + + + changeset_obj = Enum.at(timer_changeset_list, index) + + try do + start = Timex.parse!(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) + stop = Timex.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S", :strftime) + + case NaiveDateTime.compare(start, stop) do + :lt -> + # Creates a list of all other timers to check for overlap + other_timers_list = List.delete_at(timer_changeset_list, index) + + # Timer overlap verification + try do + for chs <- other_timers_list do + chs_start = chs.data.start + chs_stop = chs.data.stop + + # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) + # so no timer overlaps one another + compareStartAEndB = NaiveDateTime.compare(start, chs_stop) + compareEndAStartB = NaiveDateTime.compare(stop, chs_start) + + if( + (compareStartAEndB == :lt || compareStartAEndB == :eq) && + (compareEndAStartB == :gt || compareEndAStartB == :eq) + ) do + throw(:overlap) + end + end + + update_timer(%{id: timer_id, start: start, stop: stop}) + {:ok, []} + # {:noreply, assign(socket, editing: nil, editing_timers: [])} + catch + :overlap -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other", + :update + ) + + {:overlap, updated_changeset_timers_list} + # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} + end + + :eq -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start or stop are equal.", + :update + ) + + {:start_equal_stop, updated_changeset_timers_list} + # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} + + :gt -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start is newer that stop.", + :update + ) + + {:start_greater_than_stop, updated_changeset_timers_list} + # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} + end + rescue + _e -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Date format invalid on either start or stop.", + :update + ) + + {:invalid_format, updated_changeset_timers_list} + # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} + end + end + @doc """ Lists all the timers changesets from a given item.id. This is useful for form validation, as it returns the timers in a changeset form, in which you can add errors. @@ -104,6 +207,7 @@ defmodule App.Timer do @doc """ Errors a specific changeset from a list of changesets and returns the updated list of changesets. + Should only be called for form validation purposes You should pass a: - `timer_changeset_list: list of timer changesets to be updated - `changeset_to_error`: changeset object that you want to error out @@ -113,14 +217,13 @@ defmodule App.Timer do - `action`: action atom to apply to errored changeset. """ def error_timer_changeset( - timer_changeset_list, - changeset_to_error, - changeset_index, - error_key, - error_message, - action - ) do - + timer_changeset_list, + changeset_to_error, + changeset_index, + error_key, + error_message, + action + ) do # Clearing and adding error to changeset cleared_changeset = Map.put(changeset_to_error, :errors, []) diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 458f6a22..36e454e9 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -191,102 +191,16 @@ defmodule AppWeb.AppLive do }, socket ) do + timer_changeset_list = socket.assigns.editing_timers index = String.to_integer(index) - changeset_obj = Enum.at(timer_changeset_list, index) - - try do - start = - Timex.parse!(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) - stop = - Timex.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S", :strftime) - - case NaiveDateTime.compare(start, stop) do - :lt -> - # Creates a list of all other timers to check for overlap - other_timers_list = - List.delete_at(socket.assigns.editing_timers, index) - - # Timer overlap verification - try do - for chs <- other_timers_list do - chs_start = chs.data.start - chs_stop = chs.data.stop - - # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) - # so no timer overlaps one another - compareStartAEndB = NaiveDateTime.compare(start, chs_stop) - compareEndAStartB = NaiveDateTime.compare(stop, chs_start) - - if( - (compareStartAEndB == :lt || compareStartAEndB == :eq) && - (compareEndAStartB == :gt || compareEndAStartB == :eq) - ) do - throw(:overlap) - end - end - - Timer.update_timer(%{id: id, start: start, stop: stop}) - {:noreply, assign(socket, editing: nil, editing_timers: [])} - catch - :overlap -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other", - :update - ) - - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} - end - - :eq -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Start or stop are equal.", - :update - ) - - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} - - :gt -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Start is newer that stop.", - :update - ) - - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} - end - rescue - _e -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Date format invalid on either start or stop.", - :update - ) - - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} + case Timer.update_timer_inside_changeset_list(id, timer_start, timer_stop, index, timer_changeset_list) do + {:invalid_format, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:start_greater_than_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:start_equal_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:overlap, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:ok, _list} -> {:noreply, assign(socket, editing: nil, editing_timers: [])} end end From 0e5a49ebba73034709d4374cebfccf42eeea3a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 13:28:55 +0000 Subject: [PATCH 41/53] fix: Refactoring erroring changeset in Timer.ex. #195 --- lib/app/timer.ex | 158 ++++++++++++++++------------ lib/app_web/live/app_live.ex | 10 +- test/app_web/live/app_live_test.exs | 16 ++- 3 files changed, 111 insertions(+), 73 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 4379220f..a76151ac 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -88,98 +88,124 @@ defmodule App.Timer do timer_changeset_list ) do - + # Getting the changeset to change in case there's an error changeset_obj = Enum.at(timer_changeset_list, index) try do - start = Timex.parse!(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) - stop = Timex.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S", :strftime) + + # Parsing the dates + {start_op, start} = + Timex.parse(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) + + {stop_op, stop} = Timex.parse(timer_stop, "%Y-%m-%dT%H:%M:%S", :strftime) + + # Error guards when parsing the dates + if start_op === :error do + throw(:error_invalid_start) + end + + if stop_op === :error do + throw(:error_invalid_stop) + end case NaiveDateTime.compare(start, stop) do :lt -> + # Creates a list of all other timers to check for overlap other_timers_list = List.delete_at(timer_changeset_list, index) # Timer overlap verification - try do - for chs <- other_timers_list do - chs_start = chs.data.start - chs_stop = chs.data.stop - - # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) - # so no timer overlaps one another - compareStartAEndB = NaiveDateTime.compare(start, chs_stop) - compareEndAStartB = NaiveDateTime.compare(stop, chs_start) - - if( - (compareStartAEndB == :lt || compareStartAEndB == :eq) && - (compareEndAStartB == :gt || compareEndAStartB == :eq) - ) do - throw(:overlap) - end + for chs <- other_timers_list do + chs_start = chs.data.start + chs_stop = chs.data.stop + + # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) + # so no timer overlaps one another + compareStartAEndB = NaiveDateTime.compare(start, chs_stop) + compareEndAStartB = NaiveDateTime.compare(stop, chs_start) + + if( + (compareStartAEndB == :lt || compareStartAEndB == :eq) && + (compareEndAStartB == :gt || compareEndAStartB == :eq) + ) do + throw(:error_overlap) end - - update_timer(%{id: timer_id, start: start, stop: stop}) - {:ok, []} - # {:noreply, assign(socket, editing: nil, editing_timers: [])} - catch - :overlap -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other", - :update - ) - - {:overlap, updated_changeset_timers_list} - # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} end + update_timer(%{id: timer_id, start: start, stop: stop}) + {:ok, []} + :eq -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Start or stop are equal.", - :update - ) - - {:start_equal_stop, updated_changeset_timers_list} - # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} + throw(:error_start_equal_stop) :gt -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Start is newer that stop.", - :update - ) - - {:start_greater_than_stop, updated_changeset_timers_list} - # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} + throw(:error_start_greater_than_stop) end - rescue - _e -> + catch + :error_invalid_start -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start field has an invalid date format.", + :update + ) + + {:error_invalid_start, updated_changeset_timers_list} + + :error_invalid_stop -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Stop field has an invalid date format.", + :update + ) + + {:error_invalid_stop, updated_changeset_timers_list} + + :error_overlap -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other", + :update + ) + + {:error_overlap, updated_changeset_timers_list} + + :error_start_equal_stop -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start or stop are equal.", + :update + ) + + {:error_start_equal_stop, updated_changeset_timers_list} + + :error_start_greater_than_stop -> updated_changeset_timers_list = Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "Date format invalid on either start or stop.", + "Start is newer that stop.", :update ) - {:invalid_format, updated_changeset_timers_list} - # {:noreply, assign(socket, editing_timers: updated_changeset_timers_list)} + {:error_start_greater_than_stop, updated_changeset_timers_list} end end diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 36e454e9..88e6a0dd 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -196,11 +196,13 @@ defmodule AppWeb.AppLive do index = String.to_integer(index) case Timer.update_timer_inside_changeset_list(id, timer_start, timer_stop, index, timer_changeset_list) do - {:invalid_format, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:start_greater_than_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:start_equal_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:overlap, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} {:ok, _list} -> {:noreply, assign(socket, editing: nil, editing_timers: [])} + + {:error_invalid_start, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:error_invalid_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:error_start_greater_than_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:error_start_equal_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:error_overlap, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} end end diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index 292bf76a..4c2aa039 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -253,15 +253,25 @@ defmodule AppWeb.AppLiveTest do "timer_stop" => start }) =~ "Start is newer that stop." - # Trying to update with equal start greater than stop + # Trying to update with start as invalid format render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) assert render_submit(view, "update-item-timer", %{ "timer_id" => timer.id, "index" => 0, "timer_start" => "invalid", - "timer_stop" => "invalid" - }) =~ "Date format invalid on either start or stop." + "timer_stop" => stop + }) =~ "Start field has an invalid date format." + + # Trying to update with stop as invalid format + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => start, + "timer_stop" => "invalid" + }) =~ "Stop field has an invalid date format." end test "update timer timer with ongoing timer ", %{conn: conn} do From 6536a5fdc5314b334bc11ada1a42079669313df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 13:43:18 +0000 Subject: [PATCH 42/53] fix: Fixing tests on ongoing timers and refactoring to Timer.ex. #195 --- lib/app/timer.ex | 64 +++++++++++++++++++++++++++++ lib/app_web/live/app_live.ex | 61 +-------------------------- test/app_web/live/app_live_test.exs | 2 +- 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index a76151ac..27ab2244 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -80,6 +80,70 @@ defmodule App.Timer do |> Repo.update() end + def update_timer_inside_changeset_list( + timer_id, + timer_start, + timer_stop, + index, + timer_changeset_list + ) when timer_stop == "" or timer_stop == nil do + + # Getting the changeset to change in case there's an error + changeset_obj = Enum.at(timer_changeset_list, index) + + try do + + # Parsing the dates + {start_op, start} = + Timex.parse(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) + + # Error guards when parsing the date + if start_op === :error do + throw(:error_invalid_start) + end + + # Getting a list of the other timers (the rest we aren't updating) + other_timers_list = List.delete_at(timer_changeset_list, index) + + # Latest timer end + max_end = + other_timers_list |> Enum.map(fn chs -> chs.data.stop end) |> Enum.max() + + case NaiveDateTime.compare(start, max_end) do + :gt -> + update_timer(%{id: timer_id, start: start, stop: nil}) + {:ok, []} + + _ -> throw(:error_not_after_others) + end + catch + :error_invalid_start -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start field has an invalid date format.", + :update + ) + + {:error_invalid_start, updated_changeset_timers_list} + + :error_not_after_others -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "When editing an ongoing timer, make sure it's after all the others.", + :update + ) + + {:error_not_after_others, updated_changeset_timers_list} + end + end def update_timer_inside_changeset_list( timer_id, timer_start, diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 88e6a0dd..ef4c7893 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -121,66 +121,6 @@ defmodule AppWeb.AppLive do end @impl true - def handle_event( - "update-item-timer", - %{ - "timer_id" => id, - "index" => index, - "timer_start" => timer_start, - "timer_stop" => timer_stop - }, - socket - ) - when timer_stop == "" do - timer_changeset_list = socket.assigns.editing_timers - index = String.to_integer(index) - changeset_obj = Enum.at(timer_changeset_list, index) - - try do - start = - Timex.parse!(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) - - other_timers_list = List.delete_at(socket.assigns.editing_timers, index) - - max_end = - other_timers_list |> Enum.map(fn chs -> chs.data.stop end) |> Enum.max() - - case NaiveDateTime.compare(start, max_end) do - :gt -> - Timer.update_timer(%{id: id, start: start, stop: nil}) - {:noreply, assign(socket, editing: nil, editing_timers: [])} - - _ -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "When editing an ongoing timer, make sure it's after all the others.", - :update - ) - - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} - end - rescue - _e -> - updated_changeset_timers_list = - Timer.error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Date format invalid on either start or stop.", - :update - ) - - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} - end - end - def handle_event( "update-item-timer", %{ @@ -203,6 +143,7 @@ defmodule AppWeb.AppLive do {:error_start_greater_than_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} {:error_start_equal_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} {:error_overlap, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:error_not_after_others, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} end end diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index 4c2aa039..a88abc8d 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -356,7 +356,7 @@ defmodule AppWeb.AppLiveTest do "timer_stop" => "" }) - assert error_format_view =~ "Date format invalid on either start or stop." + assert error_format_view =~ "Start field has an invalid date format." # Update successful ----------- ten_seconds_after_string = From 71d2f4e965c2c58a34f7c35973fbae03b176d9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 13:56:15 +0000 Subject: [PATCH 43/53] fix: Adding documentation and simplifying controller pattern matching on error. #195 --- lib/app/timer.ex | 22 +++++++++++++++------- lib/app_web/live/app_live.ex | 8 +------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 27ab2244..8a7dd67e 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -80,6 +80,14 @@ defmodule App.Timer do |> Repo.update() end + @doc """ + Updates a timer object inside a list with timer changesets. + This function is only useful for form validations, since it replaces the errored changeset + according to the index that is passed, alongside the list and the fields to update the timer. + + It returns {:ok, []} in case the update is successful. + Otherwise, it returns {:error, updated_list}, where `error_term` is the error that occurred and `updated_list` being the updated item changeset list with the error. + """ def update_timer_inside_changeset_list( timer_id, timer_start, @@ -128,7 +136,7 @@ defmodule App.Timer do :update ) - {:error_invalid_start, updated_changeset_timers_list} + {:error, updated_changeset_timers_list} :error_not_after_others -> updated_changeset_timers_list = @@ -141,7 +149,7 @@ defmodule App.Timer do :update ) - {:error_not_after_others, updated_changeset_timers_list} + {:error, updated_changeset_timers_list} end end def update_timer_inside_changeset_list( @@ -217,7 +225,7 @@ defmodule App.Timer do :update ) - {:error_invalid_start, updated_changeset_timers_list} + {:error, updated_changeset_timers_list} :error_invalid_stop -> updated_changeset_timers_list = @@ -230,7 +238,7 @@ defmodule App.Timer do :update ) - {:error_invalid_stop, updated_changeset_timers_list} + {:error, updated_changeset_timers_list} :error_overlap -> updated_changeset_timers_list = @@ -243,7 +251,7 @@ defmodule App.Timer do :update ) - {:error_overlap, updated_changeset_timers_list} + {:error, updated_changeset_timers_list} :error_start_equal_stop -> updated_changeset_timers_list = @@ -256,7 +264,7 @@ defmodule App.Timer do :update ) - {:error_start_equal_stop, updated_changeset_timers_list} + {:error, updated_changeset_timers_list} :error_start_greater_than_stop -> updated_changeset_timers_list = @@ -269,7 +277,7 @@ defmodule App.Timer do :update ) - {:error_start_greater_than_stop, updated_changeset_timers_list} + {:error, updated_changeset_timers_list} end end diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index ef4c7893..9bec2198 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -137,13 +137,7 @@ defmodule AppWeb.AppLive do case Timer.update_timer_inside_changeset_list(id, timer_start, timer_stop, index, timer_changeset_list) do {:ok, _list} -> {:noreply, assign(socket, editing: nil, editing_timers: [])} - - {:error_invalid_start, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:error_invalid_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:error_start_greater_than_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:error_start_equal_stop, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:error_overlap, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} - {:error_not_after_others, updated_list} -> {:noreply, assign(socket, editing_timers: updated_list)} + {:error, updated_errored_list} -> {:noreply, assign(socket, editing_timers: updated_errored_list)} end end From 1b62bd785fbae301371d58c840fdaed41002ed4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 14:36:28 +0000 Subject: [PATCH 44/53] fix: Updating BUILD.md file. #195 --- BUILDIT.md | 1037 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 662 insertions(+), 375 deletions(-) diff --git a/BUILDIT.md b/BUILDIT.md index 09ae5a77..b2c58912 100644 --- a/BUILDIT.md +++ b/BUILDIT.md @@ -2782,303 +2782,20 @@ not just `ISO6601`. Currently, Elixir doesn't have a way to create a datetime object from any string format. For this, we are going use -[`Bluzky's datetime parser`](https://gist.github.com/bluzky/62a20cdb57b17f47c67261c10aa3da8b). - -Create a file in `lib/app/datetime_parser.ex` and -use the following code: +[`Timex`](https://github.com/bitwalker/timex). +In `mix.exs`, add the following piece of code in the `deps` section. ```elixir -# Full credit of this module goes to https://dev.to/onpointvn/build-your-own-date-time-parser-in-elixir-50be -# Do check the gist -> https://gist.github.com/bluzky/62a20cdb57b17f47c67261c10aa3da8b -defmodule App.DateTimeParser do - @mapping %{ - "H" => "(?\\d{2})", - "I" => "(?\\d{2})", - "M" => "(?\\d{2})", - "S" => "(?\\d{2})", - "d" => "(?\\d{2})", - "m" => "(?\\d{2})", - "y" => "(?\\d{2})", - "Y" => "(?-?\\d{4})", - "z" => "(?[+-]?\\d{4})", - "Z" => "(?[a-zA-Z_\/]+)", - "p" => "(?

PM|AM)", - "P" => "(?

pm|am)", - "%" => "%" - } - - @doc """ - Parse string to datetime struct - **Example** - parse("2021-20-10", "%Y-%M-%d") - Support format - | format | description| value example | - | -- | -- | -- | - | H | 24 hour | 00 - 23 | - | I | 12 hour | 00 - 12 | - | M | minute| 00 - 59 | - | S | second | 00 - 59 | - | d | day | 01 - 31 | - | m | month | 01 -12 | - | y | 2 digits year | 00 - 99 | - | Y | 4 digits year | | - | z | timezone offset | +0100, -0330 | - | Z | timezone name | UTC+7, Asia/Ho_Chi_Minh | - | p | PM or AM | | - | P | pm or am | | - """ - def parse!(dt_string, format \\ "%Y-%m-%dT%H:%M:%SZ") do - case parse(dt_string, format) do - {:ok, dt} -> - dt - - {:error, message} -> - raise "Parse string #{dt_string} with error: #{message}" - end - end - - @doc """ - Parses the string according to the format. Pipes through regex compilation, casts each part of the string to a named regex capture and tries to convert to datetime. - """ - def parse(dt_string, format \\ "%Y-%m-%dT%H:%M:%SZ") do - format - |> build_regex - |> Regex.named_captures(dt_string) - |> cast_data - |> to_datetime - end - - @doc """ - Builds the regex expression to later be captured (have named key-value captures).any() - This uses the @mapping structure to name specific parts of the entered string to convert to datetime. - """ - def build_regex(format) do - keys = Map.keys(@mapping) |> Enum.join("") - - Regex.compile!("([^%]*)%([#{keys}])([^%]*)") - |> Regex.scan(format) - |> Enum.map(fn [_, s1, key, s2] -> - [s1, Map.get(@mapping, key), s2] - end) - |> to_string() - |> Regex.compile!() - end - - @default_value %{ - day: 1, - month: 1, - year: 0, - hour: 0, - minute: 0, - second: 0, - utc_offset: 0, - tz_name: "UTC", - shift: "AM" - } - def cast_data(nil), do: {:error, "invalid datetime"} - - @doc """ - Casts each capture of the regex to appropriated format (compatible with DateTime struct) - """ - def cast_data(captures) do - captures - |> Enum.reduce_while([], fn {part, value}, acc -> - {:ok, data} = cast(part, value) - {:cont, [data | acc]} - end) - |> Enum.into(@default_value) - end - - @value_rages %{ - "hour" => [0, 23], - "hour12" => [0, 12], - "minute" => [0, 59], - "second" => [0, 59], - "day" => [0, 31], - "month" => [1, 12], - "year2" => [0, 99] - } - - defp cast("P", value) do - cast("p", String.upcase(value)) - end - - defp cast("p", value) do - {:ok, {:shift, value}} - end - - defp cast("tz", value) do - {hour, minute} = String.split_at(value, 3) - - with {:ok, {_, hour}} <- cast("offset_h", hour), - {:ok, {_, minute}} <- cast("offset_m", minute) do - sign = div(hour, abs(hour)) - {:ok, {:utc_offset, sign * (abs(hour) * 3600 + minute * 60)}} - end - end - - defp cast("tz_name", value) do - {:ok, {:tz_name, value}} - end - - defp cast(part, value) do - value = String.to_integer(value) - - valid = - case Map.get(@value_rages, part) do - [min, max] -> - value >= min and value <= max - - _ -> - true - end - - if valid do - {:ok, {String.to_atom(part), value}} - end - end - - defp to_datetime({:error, _} = error), do: error - - defp to_datetime(%{year2: value} = data) do - current_year = DateTime.utc_now() |> Map.get(:year) - year = div(current_year, 100) * 100 + value - - data - |> Map.put(:year, year) - |> Map.delete(:year2) - |> to_datetime() - end - - defp to_datetime(%{hour12: hour} = data) do - # 12AM is not valid - - if hour == 12 and data.shift == "AM" do - {:error, "12AM is invalid value"} - else - hour = - cond do - hour == 12 and data.shift == "PM" -> hour - data.shift == "AM" -> hour - data.shift == "PM" -> hour + 12 - end - - data - |> Map.put(:hour, hour) - |> Map.delete(:hour12) - |> to_datetime() - end - end - - defp to_datetime(data) do - with {:ok, date} <- Date.new(data.year, data.month, data.day), - {:ok, time} <- Time.new(data.hour, data.minute, data.second), - {:ok, datetime} <- DateTime.new(date, time) do - datetime = DateTime.add(datetime, -data.utc_offset, :second) - - if data.tz_name != "UTC" do - {:error, "Only UTC timezone is available"} - else - {:ok, datetime} - end - end - end -end +{:timex, "~> 3.7"}, ``` -With this parser, we have access to `parse/2` where we can +and run `mix deps.get`. +This will download and install the package so we can use it. + +With this library, we have access to `parse/3` where we can create a DateTime object from a string according to a given format. We are going to be using this later on. -For now, let's add some tests to this module. -Create a new test file in -`test/app/datetime_parser_test.exs` -and add the following tests. - -```elixir -defmodule App.DateTimeParserTest do - use App.DataCase - alias App.DateTimeParser - - test "valid parse of valid datetime" do - parsed_time = - DateTimeParser.parse!("2022-10-27 14:47:56", "%Y-%m-%d %H:%M:%S") - - {:ok, expected_datetime, 0} = DateTime.from_iso8601("2022-10-27T14:47:56Z") - - assert parsed_time == expected_datetime - end - - test "valid parse of valid date with %Y-%m-%d format" do - parsed_time = DateTimeParser.parse!("2022-10-27", "%Y-%m-%d") - {:ok, expected_datetime, 0} = DateTime.from_iso8601("2022-10-27T00:00:00Z") - - assert parsed_time == expected_datetime - end - - test "non-compatible regex when parsing" do - assert_raise Regex.CompileError, fn -> - DateTimeParser.parse!("2022-10-27 14:47:56", "%Y-%Y-%Y") - end - end - - test "invalid datetime format" do - assert_raise RuntimeError, fn -> - DateTimeParser.parse!("2022-102-2752 1423:4127:56", "%Y-%m-%d %H:%M:%S") - end - end - - test "valid timezone offset (with tz)" do - parsed_date = - DateTimeParser.parse!("2022-10-27T00:00:00Z+0230", "%Y-%m-%dT%H:%M:%SZ%z") - - {:ok, expected_datetime, 9000} = - DateTime.from_iso8601("2022-10-27T00:00:00+02:30") - - assert parsed_date == expected_datetime - end - - test "valid timezone offset (with UTC)" do - parsed_date = - DateTimeParser.parse!( - "2022-10-27T00:00:00ZUTC+0230", - "%Y-%m-%dT%H:%M:%SZ%Z" - ) - - assert parsed_date == ~U[2022-10-27 00:00:00Z] - end - - test "invalid timezone name" do - assert_raise RuntimeError, fn -> - DateTimeParser.parse!( - "2022-10-27T00:00:00ZEtc+0230", - "%Y-%m-%dT%H:%M:%SZ%Z" - ) - end - end - - test "valid datetime with PM/AM" do - date_under = DateTimeParser.parse!("2022-10-27 06:02pm", "%Y-%m-%d %H:%M%P") - date_sup = DateTimeParser.parse!("2022-10-27 06:02PM", "%Y-%m-%d %H:%M%p") - - assert date_under == date_sup - end - - test "valid datetime with PM/AM with two digits" do - parsed_datetime = - DateTimeParser.parse!("2022-10-27 06:02pm", "%Y-%m-%d %I:%M%P") - - assert parsed_datetime == ~U[2022-10-27 18:02:00Z] - end - - test "valid datetime with two-digit year" do - parsed_date = DateTimeParser.parse!("10-10-27", "%y-%m-%d") - - assert parsed_date == ~U[2010-10-27 00:00:00Z] - end -end -``` - ## 12.2 Persisting update in database So far we can only start, stop and fetch timers. We need a way to directly update a specific timer through their `id`. @@ -3117,17 +2834,30 @@ This will properly reference `Timer` to the `Item` object. end ``` -In the same file, let us add a way to list all the timers associated -with a certain `item` id. Paste the following. +In the same file, let us add a way to list all the timer changesets associated +with a certain `item` id. +We are returning changesets because of form validation. +In case an error occurs, we want to provide feedback to the user. +To do this, we use these changesets and add errors to them, +which will later be displayed on the UI. +Paste the following. ```elixir - def list_timers(item_id) do + def list_timers_changesets(item_id) do from(v in Timer, where: [item_id: ^item_id], order_by: [asc: :id]) |> Repo.all() - end + |> Enum.map(fn t -> + Timer.changeset(t, %{ + id: t.id, + start: t.start, + stop: t.stop, + item_id: t.item_id + }) + end) + end ``` -## 12.3 Showing timers in UI +## 12.3 Adding event handler in `app_live.ex` We need a way to show the timers related to an `item` in the UI. Currently, in `lib/app_web/live/app_live.ex`, every time the user edits an item, an `edit-timer` event is propped up, setting the @@ -3136,7 +2866,7 @@ socket assigns accordingly. We want to fetch the timers of an item *ad-hoc*. Instead of loading all the timers on mount, it's best to dynamically fetch the timers whenever we want to edit a timer. For this, we are going to add an -**array of timer changesets** to the docket assigns and show these +**array of timer changesets** to the socket assigns and show these when editing a timer. Let's do that. In `lib/app_web/live/app_live.ex`, in the `mount` function, add @@ -3153,24 +2883,14 @@ In `lib/app_web/live/app_live.ex`, in the `mount` function, add ``` Let's change the `handle_event` handler for the `edit-item` event -to fetch the timers when editing an item. Change the function +to fetch the timer changesets when editing an item. Change the function to the following: ```elixir def handle_event("edit-item", data, socket) do item_id = String.to_integer(data["id"]) - timers_list = Timer.list_timers(item_id) - - timers_list_changeset = - Enum.map(timers_list, fn t -> - Timer.changeset(t, %{ - id: t.id, - start: t.start, - stop: t.stop, - item_id: t.item_id - }) - end) + timers_list_changeset = Timer.list_timers_changesets(item_id) {:noreply, assign(socket, editing: item_id, editing_timers: timers_list_changeset)} @@ -3201,101 +2921,387 @@ in the same file. }, socket ) do + timer_changeset_list = socket.assigns.editing_timers index = String.to_integer(index) + + case Timer.update_timer_inside_changeset_list(id, timer_start, timer_stop, index, timer_changeset_list) do + {:ok, _list} -> {:noreply, assign(socket, editing: nil, editing_timers: [])} + {:error, updated_errored_list} -> {:noreply, assign(socket, editing_timers: updated_errored_list)} + end + end +``` + +Let's do a rundown of what we just added. +From the form, we receive an `index` of the timer inside the `editing_timers` +socket assign array. We use this `index` to replace the changeset being edited +in case there's an error with the string format or the dates. + +We are calling a function `update_timer_inside_changeset_list/5` +that we will implement shortly, This function will either +update the timer successfully or return an error, +with an updated list of timer changesets to display the error on the UI. + +We want the users to be able to update timers even when +there's an ongoing timer and have the users still +see the list of timers. +For this, we ought to update the events that are created +when clicking `Resume` or `Stop`. +Therefore, we need to these handlers and the broadcast +`update` event that is sent to all users. + +Let's check the `start` and `stop` event handlers inside `app_live.ex`. +Let's add information to the event with the `item.id` that is being edited. +Change these event handlers so they look like this. + +```elixir + @impl true + def handle_event("start", data, socket) do + item = Item.get_item!(Map.get(data, "id")) + person_id = get_person_id(socket.assigns) + + {:ok, _timer} = + Timer.start(%{ + item_id: item.id, + person_id: person_id, + start: NaiveDateTime.utc_now() + }) + + AppWeb.Endpoint.broadcast(@topic, "update", {:start, item.id}) + {:noreply, socket} + end + + @impl true + def handle_event("stop", data, socket) do + timer_id = Map.get(data, "timerid") + {:ok, _timer} = Timer.stop(%{id: timer_id}) + + AppWeb.Endpoint.broadcast(@topic, "update", {:stop, Map.get(data, "id")}) + {:noreply, socket} + end +``` + +Now we need to update the `handle_info/2` event handler +that deals with this broadcasting event that is used +everytime `Start/Resume` or `Stop` is called. + +```elixir + @impl true + def handle_info(%Broadcast{event: "update", payload: payload}, socket) do + person_id = get_person_id(socket.assigns) + items = Item.items_with_timers(person_id) + + isEditingItem = socket.assigns.editing + + # If the item is being edited, we update the timer list of the item being edited. + if isEditingItem do + case payload do + {:start, item_id} -> + timers_list_changeset = Timer.list_timers_changesets(item_id) + + {:noreply, + assign(socket, + items: items, + editing: item_id, + editing_timers: timers_list_changeset + )} + + {:stop, item_id} -> + timers_list_changeset = Timer.list_timers_changesets(item_id) + + {:noreply, + assign(socket, + items: items, + editing: item_id, + editing_timers: timers_list_changeset + )} + + _ -> + {:noreply, assign(socket, items: items)} + end + + # If not, just update the item list. + else + {:noreply, assign(socket, items: items)} + end + end +``` + +Now, everytime the `update` event is broadcasted, +we update the timer list if the item is being edited. +If not, we update the timer list, as normally. +What this does is that every user will have the `socket.assigns` +properly updated everytime a timer is edited. + + +## 12.4 Updating timer changeset list on `timer.ex` +Let's create the unimplemented function that we +previously added. +In the `timer.ex` file, add the following. + +```elixir +def update_timer_inside_changeset_list( + timer_id, + timer_start, + timer_stop, + index, + timer_changeset_list + ) when timer_stop == "" or timer_stop == nil do + + # Getting the changeset to change in case there's an error changeset_obj = Enum.at(timer_changeset_list, index) try do - start = App.DateTimeParser.parse!(timer_start, "%Y-%m-%dT%H:%M:%S") - stop = App.DateTimeParser.parse!(timer_stop, "%Y-%m-%dT%H:%M:%S") - case DateTime.compare(start, stop) do - :lt -> - Timer.update_timer(%{id: id, start: start, stop: stop}) - {:noreply, assign(socket, editing: nil, editing_timers: [])} + # Parsing the dates + {start_op, start} = + Timex.parse(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) - :eq -> - updated_changeset_timers_list = - error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Start or stop are equal." - ) + # Error guards when parsing the date + if start_op === :error do + throw(:error_invalid_start) + end - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} + # Getting a list of the other timers (the rest we aren't updating) + other_timers_list = List.delete_at(timer_changeset_list, index) + + # Latest timer end + max_end = + other_timers_list |> Enum.map(fn chs -> chs.data.stop end) |> Enum.max() + case NaiveDateTime.compare(start, max_end) do :gt -> - updated_changeset_timers_list = - error_timer_changeset( - timer_changeset_list, - changeset_obj, - index, - :id, - "Start is newer that stop." - ) + update_timer(%{id: timer_id, start: start, stop: nil}) + {:ok, []} - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} + _ -> throw(:error_not_after_others) end - rescue - e -> + catch + :error_invalid_start -> updated_changeset_timers_list = - error_timer_changeset( + Timer.error_timer_changeset( timer_changeset_list, changeset_obj, index, :id, - "Date format invalid on either start or stop." + "Start field has an invalid date format.", + :update ) - {:noreply, - assign(socket, editing_timers: updated_changeset_timers_list)} + {:error, updated_changeset_timers_list} + + :error_not_after_others -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "When editing an ongoing timer, make sure it's after all the others.", + :update + ) + + {:error, updated_changeset_timers_list} end end -``` + def update_timer_inside_changeset_list( + timer_id, + timer_start, + timer_stop, + index, + timer_changeset_list + ) do -Let's do a rundown of what we just created. -From the form, we receive an `index` of the timer inside the `editing_timers` -socket assign array. We use this `index` to replace the changeset in case -there's an error with the string format or the dates. + # Getting the changeset to change in case there's an error + changeset_obj = Enum.at(timer_changeset_list, index) + + try do + + # Parsing the dates + {start_op, start} = + Timex.parse(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) + + {stop_op, stop} = Timex.parse(timer_stop, "%Y-%m-%dT%H:%M:%S", :strftime) + + # Error guards when parsing the dates + if start_op === :error do + throw(:error_invalid_start) + end + + if stop_op === :error do + throw(:error_invalid_stop) + end + + case NaiveDateTime.compare(start, stop) do + :lt -> + + # Creates a list of all other timers to check for overlap + other_timers_list = List.delete_at(timer_changeset_list, index) + + # Timer overlap verification + for chs <- other_timers_list do + chs_start = chs.data.start + chs_stop = chs.data.stop + + # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) + # so no timer overlaps one another + compareStartAEndB = NaiveDateTime.compare(start, chs_stop) + compareEndAStartB = NaiveDateTime.compare(stop, chs_start) + + if( + (compareStartAEndB == :lt || compareStartAEndB == :eq) && + (compareEndAStartB == :gt || compareEndAStartB == :eq) + ) do + throw(:error_overlap) + end + end -We try to parse the files using the specified format. If this succeeds, we compare -the `start` and `stop` parameters and check if `start` does not start after `stop`. -If everything is correct, the `Timer` is updated corretly. -If not, the timer changeset is updated with the error and replaced in the array. -We use the `error_timer_changeset/4` function to do that. Let's create it. + update_timer(%{id: timer_id, start: start, stop: stop}) + {:ok, []} + + :eq -> + throw(:error_start_equal_stop) + + :gt -> + throw(:error_start_greater_than_stop) + end + catch + :error_invalid_start -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start field has an invalid date format.", + :update + ) + + {:error, updated_changeset_timers_list} + + :error_invalid_stop -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Stop field has an invalid date format.", + :update + ) + + {:error, updated_changeset_timers_list} + + :error_overlap -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "This timer interval overlaps with other timers. Make sure all the timers are correct and don't overlap with each other", + :update + ) + + {:error, updated_changeset_timers_list} + + :error_start_equal_stop -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start or stop are equal.", + :update + ) + + {:error, updated_changeset_timers_list} + + :error_start_greater_than_stop -> + updated_changeset_timers_list = + Timer.error_timer_changeset( + timer_changeset_list, + changeset_obj, + index, + :id, + "Start is newer that stop.", + :update + ) + + {:error, updated_changeset_timers_list} + end + end +``` + +That is a lot of code! But it's fairly simple. +Firstly, these two functions are called according to +pattern matching of the `timer_stop` field. +If `timer_stop` field is empty, we assume it's an +ongoing timer being edited. +If both `timer_start` and `timer_stop` is being edited, +it's because the user is changing an old timer. + +Inside both functions, the flow is the same. +We first get the *timer changeset* being edited +by using the `index` parameter and the passed changeset list. +After this, we try to parse the field using `Timex`. +If this doesn't work, we **throw an error**. +All of errors thrown are later catched. + +If the parse is successful, we compare the +`start` and `stop` fields and check if the `start` +is newer than `stop` or if they're equal. +This is not allowed, so we throw an error if this is the case. + +If these verifications are passed, in the case of +*ongoing timers*, we check if the timer `start` being edited +is **after all the timers**. +In the case of *old timer being updated*, +we check if there is an overlap with the rest of the timers. + +If all of these validations are successful, +the timer is updated. +If not, the error that was thrown is caught +using `catch`. +Depending on the error, we add a different error text +to be displayed on the form and then return the error. + +In each error, we make use of the `error_timer_changeset/6` +function, which just replaces the timer inside the list +with a custom error to be displayed on the form. +Let's add this function. ```elixir - defp error_timer_changeset( - timer_changeset_list, - changeset_to_error, - changeset_index, - changeset_error_key, - changeset_error_message - ) do - # Adding error to changeset + def error_timer_changeset( + timer_changeset_list, + changeset_to_error, + changeset_index, + error_key, + error_message, + action + ) do + # Clearing and adding error to changeset + cleared_changeset = Map.put(changeset_to_error, :errors, []) + errored_changeset = Ecto.Changeset.add_error( - changeset_to_error, - changeset_error_key, - changeset_error_message + cleared_changeset, + error_key, + error_message ) {_reply, errored_changeset} = - Ecto.Changeset.apply_action(errored_changeset, :update) + Ecto.Changeset.apply_action(errored_changeset, action) # Updated list with errored changeset List.replace_at(timer_changeset_list, changeset_index, errored_changeset) end ``` -This function simply adds the specified error and updates the -socket assign `editing_timer` array with the errored changeset object. - +And now all that's left is to change the UI! Let's do that. +## 12.5 Updating the UI Now let's focus on showing the timers in the UI. Head over to `lib/app_web/live/app_live.html.heex` and make the following changes. We are showing each timer whenever an `item` is being edited. @@ -3393,7 +3399,6 @@ We are showing each timer whenever an `item` is being edited. @@ -3434,10 +3439,16 @@ We are showing each timer whenever an `item` is being edited. ``` As you can see from the snippet above, -when the changes from the form are submitted, a +for each timer related to an `item`, +we are creating a form. +When the changes from the form are submitted, a `update-item-timer` event is created. +With this event, all the fields added inside +the form is passed on (the timer `id`, +`index` inside the timer changesetlist, +`timer_start` and `timer_stop`) -## 12.4 Updating the tests and going back to 100% coverage +## 12.6 Updating the tests and going back to 100% coverage If we run `source .env_sample` and `MIX_ENV=test mix coveralls.html ; open cover/excoveralls.html` we will see how coverage dropped. @@ -3489,8 +3500,6 @@ test "update an item's timer", %{conn: conn} do {:ok, seven_seconds_ago} = NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) - {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) - # Start the timer 7 seconds ago: {:ok, timer} = Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) @@ -3535,15 +3544,25 @@ test "update an item's timer", %{conn: conn} do "timer_stop" => start }) =~ "Start is newer that stop." - # Trying to update with equal start greater than stop + # Trying to update with start as invalid format render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) assert render_submit(view, "update-item-timer", %{ "timer_id" => timer.id, "index" => 0, "timer_start" => "invalid", - "timer_stop" => "invalid" - }) =~ "Date format invalid on either start or stop." + "timer_stop" => stop + }) =~ "Start field has an invalid date format." + + # Trying to update with stop as invalid format + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => start, + "timer_stop" => "invalid" + }) =~ "Stop field has an invalid date format." end ``` @@ -3562,7 +3581,275 @@ Change the test to the following. end ``` -You should now have a function way to change the timers! :wink: +Let's add more tests for the edge cases. +Let's test ongoing timers and overlapping :smile: +In the same `test_live_test.exs` file, +add the following tests. + +```elixir +test "update timer timer with ongoing timer ", %{conn: conn} do + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + {:ok, four_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -4)) + + {:ok, ten_seconds_after} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), 10)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + # Start a second timer + {:ok, timer2} = Timer.start(%{item_id: item.id, person_id: 1, start: now}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + {:ok, view, _html} = live(conn, "/") + + # Update fails because of overlap timer ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + four_seconds_ago_string = + NaiveDateTime.truncate(four_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + now_string = + NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + error_view = + render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => four_seconds_ago_string, + "timer_stop" => "" + }) + + assert error_view =~ "When editing an ongoing timer" + + # Update fails because of format ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + error_format_view = + render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => "invalidformat", + "timer_stop" => "" + }) + + assert error_format_view =~ "Start field has an invalid date format." + + # Update successful ----------- + ten_seconds_after_string = + NaiveDateTime.truncate(ten_seconds_after, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + ten_seconds_after_datetime = + NaiveDateTime.truncate(ten_seconds_after, :second) + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + view = + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 1, + "timer_start" => ten_seconds_after_string, + "timer_stop" => "" + }) + + updated_timer2 = Timer.get_timer!(timer2.id) + + assert updated_timer2.start == ten_seconds_after_datetime + end + + test "timer overlap error when updating timer", %{conn: conn} do + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + {:ok, four_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -4)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + # Start a second timer + {:ok, timer2} = Timer.start(%{item_id: item.id, person_id: 1, start: now}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + {:ok, view, _html} = live(conn, "/") + + # Update fails because of overlap ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + four_seconds_ago_string = + NaiveDateTime.truncate(four_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + now_string = + NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer2.id, + "index" => 0, + "timer_start" => four_seconds_ago_string, + "timer_stop" => now_string + }) =~ "This timer interval overlaps with other timers." + end +``` + +Let us not forget we also changed the way +the `update` event is broadcasted. +It now updates the socket assigns depending +on whether an item is being edited or not. +Let's add tests in the same file to cover these scenarios + +```elixir +test "handle_info/2 update with editing open (start)", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + {:ok, item} = + Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) + + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + now_string = + NaiveDateTime.truncate(now, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + render_click(view, "start", %{"id" => Integer.to_string(item.id)}) + + # The editing panel is open and showing the newly created timer on the 'Start' text input field + assert render(view) =~ now_string + end + + test "handle_info/2 update with editing open (stop)", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + {:ok, item} = + Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + render_click(view, "start", %{"id" => Integer.to_string(item.id)}) + render_click(view, "stop", %{"timerid" => timer.id, "id" => item.id}) + + num_timers_rendered = + (render(view) |> String.split("Update") |> length()) - 1 + + # Checking if two timers were rendered + assert num_timers_rendered = 2 + end + + test "handle_info/2 update with editing open (delete)", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + {:ok, item} = + Item.create_item(%{text: "Always Learning", person_id: 0, status: 2}) + + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + send(view.pid, %Broadcast{ + event: "update", + payload: :delete + }) + + assert render(view) =~ item.text + end +``` # 13. Run the _Finished_ MVP App! From 10f400b1bc55af86f244ff2480ddcf269ed852e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 14 Nov 2022 14:39:31 +0000 Subject: [PATCH 45/53] fix: Formatting files. --- lib/app/timer.ex | 22 ++++++++++------------ lib/app_web/live/app_live.ex | 18 ++++++++++++------ test/app_web/live/app_live_test.exs | 10 +++++----- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 8a7dd67e..b62f43d9 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -89,18 +89,17 @@ defmodule App.Timer do Otherwise, it returns {:error, updated_list}, where `error_term` is the error that occurred and `updated_list` being the updated item changeset list with the error. """ def update_timer_inside_changeset_list( - timer_id, - timer_start, - timer_stop, - index, - timer_changeset_list - ) when timer_stop == "" or timer_stop == nil do - + timer_id, + timer_start, + timer_stop, + index, + timer_changeset_list + ) + when timer_stop == "" or timer_stop == nil do # Getting the changeset to change in case there's an error changeset_obj = Enum.at(timer_changeset_list, index) try do - # Parsing the dates {start_op, start} = Timex.parse(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) @@ -122,7 +121,8 @@ defmodule App.Timer do update_timer(%{id: timer_id, start: start, stop: nil}) {:ok, []} - _ -> throw(:error_not_after_others) + _ -> + throw(:error_not_after_others) end catch :error_invalid_start -> @@ -152,6 +152,7 @@ defmodule App.Timer do {:error, updated_changeset_timers_list} end end + def update_timer_inside_changeset_list( timer_id, timer_start, @@ -159,12 +160,10 @@ defmodule App.Timer do index, timer_changeset_list ) do - # Getting the changeset to change in case there's an error changeset_obj = Enum.at(timer_changeset_list, index) try do - # Parsing the dates {start_op, start} = Timex.parse(timer_start, "%Y-%m-%dT%H:%M:%S", :strftime) @@ -182,7 +181,6 @@ defmodule App.Timer do case NaiveDateTime.compare(start, stop) do :lt -> - # Creates a list of all other timers to check for overlap other_timers_list = List.delete_at(timer_changeset_list, index) diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 9bec2198..09eb2a1f 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -131,18 +131,24 @@ defmodule AppWeb.AppLive do }, socket ) do - timer_changeset_list = socket.assigns.editing_timers index = String.to_integer(index) - case Timer.update_timer_inside_changeset_list(id, timer_start, timer_stop, index, timer_changeset_list) do - {:ok, _list} -> {:noreply, assign(socket, editing: nil, editing_timers: [])} - {:error, updated_errored_list} -> {:noreply, assign(socket, editing_timers: updated_errored_list)} + case Timer.update_timer_inside_changeset_list( + id, + timer_start, + timer_stop, + index, + timer_changeset_list + ) do + {:ok, _list} -> + {:noreply, assign(socket, editing: nil, editing_timers: [])} + + {:error, updated_errored_list} -> + {:noreply, assign(socket, editing_timers: updated_errored_list)} end end - - @impl true def handle_info(%Broadcast{event: "update", payload: payload}, socket) do person_id = get_person_id(socket.assigns) diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index a88abc8d..75c64b81 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -267,11 +267,11 @@ defmodule AppWeb.AppLiveTest do render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) assert render_submit(view, "update-item-timer", %{ - "timer_id" => timer.id, - "index" => 0, - "timer_start" => start, - "timer_stop" => "invalid" - }) =~ "Stop field has an invalid date format." + "timer_id" => timer.id, + "index" => 0, + "timer_start" => start, + "timer_stop" => "invalid" + }) =~ "Stop field has an invalid date format." end test "update timer timer with ongoing timer ", %{conn: conn} do From 358d85a3b7bab5040299563fb2fbee8a1e61c78f Mon Sep 17 00:00:00 2001 From: LuchoTurtle Date: Tue, 15 Nov 2022 13:08:32 +0000 Subject: [PATCH 46/53] fix: Commenting adding Timex as dependency. Co-authored-by: Nelson <194400+nelsonic@users.noreply.github.com> --- mix.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/mix.exs b/mix.exs index 2932e98f..fcec289d 100644 --- a/mix.exs +++ b/mix.exs @@ -54,6 +54,7 @@ defmodule App.MixProject do {:telemetry_poller, "~> 1.0"}, {:jason, "~> 1.2"}, {:plug_cowboy, "~> 2.5"}, + # Time string parsing: github.com/bitwalker/timex {:timex, "~> 3.7"}, # Check/get Environment Variables: https://github.com/dwyl/envar From 71592dbc9c2bc9f030cbfde6c7759b64b3b33ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 15 Nov 2022 13:12:43 +0000 Subject: [PATCH 47/53] fix: Fixing test warning on "handle_info/2 update with editing open". The assertion was an assignment, not a comparison. --- BUILDIT.md | 2 +- test/app_web/live/app_live_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDIT.md b/BUILDIT.md index b2c58912..15d8517b 100644 --- a/BUILDIT.md +++ b/BUILDIT.md @@ -3831,7 +3831,7 @@ test "handle_info/2 update with editing open (start)", %{conn: conn} do (render(view) |> String.split("Update") |> length()) - 1 # Checking if two timers were rendered - assert num_timers_rendered = 2 + assert num_timers_rendered == 2 end test "handle_info/2 update with editing open (delete)", %{conn: conn} do diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index 75c64b81..36c893fc 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -151,7 +151,7 @@ defmodule AppWeb.AppLiveTest do (render(view) |> String.split("Update") |> length()) - 1 # Checking if two timers were rendered - assert num_timers_rendered = 2 + assert num_timers_rendered == 2 end test "handle_info/2 update with editing open (delete)", %{conn: conn} do From 60ede765a5a0ef93b54f3523e0a721364f580e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Tue, 15 Nov 2022 13:50:28 +0000 Subject: [PATCH 48/53] fix: Pattern matching on `update_timer_inside_changeset_list`. Passing a timer object and pattern match on function to make it more readable. --- BUILDIT.md | 24 +++++++++++++++++------- lib/app/timer.ex | 16 ++++++++++------ lib/app_web/live/app_live.ex | 10 +++++++--- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/BUILDIT.md b/BUILDIT.md index 15d8517b..d0858893 100644 --- a/BUILDIT.md +++ b/BUILDIT.md @@ -2925,7 +2925,13 @@ in the same file. timer_changeset_list = socket.assigns.editing_timers index = String.to_integer(index) - case Timer.update_timer_inside_changeset_list(id, timer_start, timer_stop, index, timer_changeset_list) do + timer = %{ + id: id, + start: timer_start, + stop: timer_stop + } + + case Timer.update_timer_inside_changeset_list( timer, index, timer_changeset_list) do {:ok, _list} -> {:noreply, assign(socket, editing: nil, editing_timers: [])} {:error, updated_errored_list} -> {:noreply, assign(socket, editing_timers: updated_errored_list)} end @@ -3041,9 +3047,11 @@ In the `timer.ex` file, add the following. ```elixir def update_timer_inside_changeset_list( - timer_id, - timer_start, - timer_stop, + %{ + id: timer_id, + start: timer_start, + stop: timer_stop + }, index, timer_changeset_list ) when timer_stop == "" or timer_stop == nil do @@ -3105,9 +3113,11 @@ def update_timer_inside_changeset_list( end end def update_timer_inside_changeset_list( - timer_id, - timer_start, - timer_stop, + %{ + id: timer_id, + start: timer_start, + stop: timer_stop + }, index, timer_changeset_list ) do diff --git a/lib/app/timer.ex b/lib/app/timer.ex index b62f43d9..1f75716e 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -89,9 +89,11 @@ defmodule App.Timer do Otherwise, it returns {:error, updated_list}, where `error_term` is the error that occurred and `updated_list` being the updated item changeset list with the error. """ def update_timer_inside_changeset_list( - timer_id, - timer_start, - timer_stop, + %{ + id: timer_id, + start: timer_start, + stop: timer_stop + }, index, timer_changeset_list ) @@ -154,9 +156,11 @@ defmodule App.Timer do end def update_timer_inside_changeset_list( - timer_id, - timer_start, - timer_stop, + %{ + id: timer_id, + start: timer_start, + stop: timer_stop + }, index, timer_changeset_list ) do diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex index 09eb2a1f..060120c1 100644 --- a/lib/app_web/live/app_live.ex +++ b/lib/app_web/live/app_live.ex @@ -134,10 +134,14 @@ defmodule AppWeb.AppLive do timer_changeset_list = socket.assigns.editing_timers index = String.to_integer(index) + timer = %{ + id: id, + start: timer_start, + stop: timer_stop + } + case Timer.update_timer_inside_changeset_list( - id, - timer_start, - timer_stop, + timer, index, timer_changeset_list ) do From 677f68d8916a4d2954e249ea2617b791366380f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Sat, 19 Nov 2022 21:15:58 +0000 Subject: [PATCH 49/53] fix: Fixing editing historical timer while there is an ongoing timer. --- lib/app/timer.ex | 36 ++++++++++++----- test/app_web/live/app_live_test.exs | 63 +++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 1f75716e..3a4b67a9 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -188,21 +188,35 @@ defmodule App.Timer do # Creates a list of all other timers to check for overlap other_timers_list = List.delete_at(timer_changeset_list, index) - # Timer overlap verification + # Timer overlap verification --------- for chs <- other_timers_list do chs_start = chs.data.start chs_stop = chs.data.stop - # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) - # so no timer overlaps one another - compareStartAEndB = NaiveDateTime.compare(start, chs_stop) - compareEndAStartB = NaiveDateTime.compare(stop, chs_start) - - if( - (compareStartAEndB == :lt || compareStartAEndB == :eq) && - (compareEndAStartB == :gt || compareEndAStartB == :eq) - ) do - throw(:error_overlap) + # If the timer being compared is ongoing + if (chs_stop == nil) do + compareStart = NaiveDateTime.compare(start, chs_start) + compareEnd = NaiveDateTime.compare(stop, chs_start) + + # The condition needs to FAIL so the timer doesn't overlap + if (compareStart == :lt && compareEnd == :gt) do + throw(:error_overlap) + end + + # Else the timer being compared is historical + else + + # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) + # so no timer overlaps one another + compareStartAEndB = NaiveDateTime.compare(start, chs_stop) + compareEndAStartB = NaiveDateTime.compare(stop, chs_start) + + if( + (compareStartAEndB == :lt || compareStartAEndB == :eq) && + (compareEndAStartB == :gt || compareEndAStartB == :eq) + ) do + throw(:error_overlap) + end end end diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index 36c893fc..3bdc3d79 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -457,6 +457,69 @@ defmodule AppWeb.AppLiveTest do }) =~ "This timer interval overlaps with other timers." end + test "timer overlap error when updating historical timer with ongoing timer", %{conn: conn} do + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + {:ok, twenty_seconds_future} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), 20)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + # Start a second timer + {:ok, timer2} = Timer.start(%{item_id: item.id, person_id: 1, start: now}) + + {:ok, view, _html} = live(conn, "/") + + # Update fails because of overlap ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + seven_seconds_ago_string = + NaiveDateTime.truncate(seven_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + twenty_seconds_string = + NaiveDateTime.truncate(twenty_seconds_future, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => seven_seconds_ago_string, + "timer_stop" => twenty_seconds_string + }) =~ "This timer interval overlaps with other timers." + end + test "timer_text(start, stop)" do timer = %{ start: ~N[2022-07-17 09:01:42.000000], From 042a6cec7c9787b7116e3eb4131a29dbcd8818df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Sat, 19 Nov 2022 21:16:37 +0000 Subject: [PATCH 50/53] fix: Formatting files. --- lib/app/timer.ex | 7 +++---- test/app_web/live/app_live_test.exs | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/app/timer.ex b/lib/app/timer.ex index 3a4b67a9..0063579c 100644 --- a/lib/app/timer.ex +++ b/lib/app/timer.ex @@ -194,18 +194,17 @@ defmodule App.Timer do chs_stop = chs.data.stop # If the timer being compared is ongoing - if (chs_stop == nil) do + if chs_stop == nil do compareStart = NaiveDateTime.compare(start, chs_start) compareEnd = NaiveDateTime.compare(stop, chs_start) # The condition needs to FAIL so the timer doesn't overlap - if (compareStart == :lt && compareEnd == :gt) do + if compareStart == :lt && compareEnd == :gt do throw(:error_overlap) end - # Else the timer being compared is historical + # Else the timer being compared is historical else - # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) # so no timer overlaps one another compareStartAEndB = NaiveDateTime.compare(start, chs_stop) diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index 3bdc3d79..de89197b 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -457,7 +457,8 @@ defmodule AppWeb.AppLiveTest do }) =~ "This timer interval overlaps with other timers." end - test "timer overlap error when updating historical timer with ongoing timer", %{conn: conn} do + test "timer overlap error when updating historical timer with ongoing timer", + %{conn: conn} do {:ok, item} = Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) From 8225674787f88fa91e3ac51c660253258fcc8f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Sat, 19 Nov 2022 21:25:51 +0000 Subject: [PATCH 51/53] fix: Updating BUILD.md file. --- BUILDIT.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 11 deletions(-) diff --git a/BUILDIT.md b/BUILDIT.md index d0858893..760d9ee1 100644 --- a/BUILDIT.md +++ b/BUILDIT.md @@ -3148,21 +3148,34 @@ def update_timer_inside_changeset_list( # Creates a list of all other timers to check for overlap other_timers_list = List.delete_at(timer_changeset_list, index) - # Timer overlap verification + # Timer overlap verification --------- for chs <- other_timers_list do chs_start = chs.data.start chs_stop = chs.data.stop - # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) - # so no timer overlaps one another - compareStartAEndB = NaiveDateTime.compare(start, chs_stop) - compareEndAStartB = NaiveDateTime.compare(stop, chs_start) - - if( - (compareStartAEndB == :lt || compareStartAEndB == :eq) && - (compareEndAStartB == :gt || compareEndAStartB == :eq) - ) do - throw(:error_overlap) + # If the timer being compared is ongoing + if chs_stop == nil do + compareStart = NaiveDateTime.compare(start, chs_start) + compareEnd = NaiveDateTime.compare(stop, chs_start) + + # The condition needs to FAIL so the timer doesn't overlap + if compareStart == :lt && compareEnd == :gt do + throw(:error_overlap) + end + + # Else the timer being compared is historical + else + # The condition needs to FAIL (StartA <= EndB) and (EndA >= StartB) + # so no timer overlaps one another + compareStartAEndB = NaiveDateTime.compare(start, chs_stop) + compareEndAStartB = NaiveDateTime.compare(stop, chs_start) + + if( + (compareStartAEndB == :lt || compareStartAEndB == :eq) && + (compareEndAStartB == :gt || compareEndAStartB == :eq) + ) do + throw(:error_overlap) + end end end @@ -3779,6 +3792,70 @@ test "update timer timer with ongoing timer ", %{conn: conn} do "timer_stop" => now_string }) =~ "This timer interval overlaps with other timers." end + + test "timer overlap error when updating historical timer with ongoing timer", + %{conn: conn} do + {:ok, item} = + Item.create_item(%{text: "Learn Elixir", person_id: 0, status: 2}) + + {:ok, seven_seconds_ago} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), -7)) + + {:ok, now} = NaiveDateTime.new(Date.utc_today(), Time.utc_now()) + + {:ok, twenty_seconds_future} = + NaiveDateTime.new(Date.utc_today(), Time.add(Time.utc_now(), 20)) + + # Start the timer 7 seconds ago: + {:ok, timer} = + Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago}) + + # Stop the timer based on its item_id + Timer.stop_timer_for_item_id(item.id) + + # Start a second timer + {:ok, timer2} = Timer.start(%{item_id: item.id, person_id: 1, start: now}) + + {:ok, view, _html} = live(conn, "/") + + # Update fails because of overlap ----------- + render_click(view, "edit-item", %{"id" => Integer.to_string(item.id)}) + + seven_seconds_ago_string = + NaiveDateTime.truncate(seven_seconds_ago, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + twenty_seconds_string = + NaiveDateTime.truncate(twenty_seconds_future, :second) + |> NaiveDateTime.to_string() + |> String.graphemes() + |> Enum.with_index() + |> Enum.map(fn {value, index} -> + if index == 10 do + "T" + else + value + end + end) + |> List.to_string() + + assert render_submit(view, "update-item-timer", %{ + "timer_id" => timer.id, + "index" => 0, + "timer_start" => seven_seconds_ago_string, + "timer_stop" => twenty_seconds_string + }) =~ "This timer interval overlaps with other timers." + end ``` Let us not forget we also changed the way From 53b7de60c4e961cdaad5afa7728ac7638a563d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Arteiro?= Date: Mon, 21 Nov 2022 10:10:34 +0000 Subject: [PATCH 52/53] fix: Fix tests for 100% coverage. --- test/app_web/live/app_live_test.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/app_web/live/app_live_test.exs b/test/app_web/live/app_live_test.exs index de89197b..7b98e75d 100644 --- a/test/app_web/live/app_live_test.exs +++ b/test/app_web/live/app_live_test.exs @@ -530,6 +530,15 @@ defmodule AppWeb.AppLiveTest do assert AppWeb.AppLive.timer_text(timer) == "04:20:42" end + test "timer_text(start, stop) over 1000 secs" do + timer = %{ + start: ~N[2022-07-17 09:01:42.000000], + stop: ~N[2022-07-17 09:19:24.000000] + } + + assert AppWeb.AppLive.timer_text(timer) == "00:17:42" + end + test "filter items", %{conn: conn} do {:ok, _item} = Item.create_item(%{text: "Item to do", person_id: 0, status: 2}) From 38b26ae73b502d39083c71a7ee41d30f3f566b0b Mon Sep 17 00:00:00 2001 From: Nelson <194400+nelsonic@users.noreply.github.com> Date: Mon, 21 Nov 2022 11:07:01 +0000 Subject: [PATCH 53/53] remove "environment: dwylauth" from ci.yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3aa87a69..96a5a36d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,6 @@ on: jobs: build: name: Build and test - environment: dwylauth runs-on: ubuntu-latest services: postgres: