From bb272c7b4b70dc141c78e1327972d555be147314 Mon Sep 17 00:00:00 2001 From: Mathias Wingert Date: Wed, 18 Sep 2024 22:31:09 +0200 Subject: [PATCH 1/4] feature: Soft deleting episode Marking episodes as `is_deleted = true` and setting `deleted_at = now`. `list_available_episodes` gives all episodes that are NOT marked as deleted. `list_all_episodes` gives all episodes including the once that are marked as deleted. --- lib/radiator/podcast.ex | 63 ++++++++++++++++--- lib/radiator/podcast/episode.ex | 20 +++++- lib/radiator_web/live/episode_live/index.ex | 43 ++++++++++++- ...40918192918_add_is_deleted_to_episodes.exs | 10 +++ test/radiator/podcast_test.exs | 28 ++++++++- 5 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 priv/repo/migrations/20240918192918_add_is_deleted_to_episodes.exs diff --git a/lib/radiator/podcast.ex b/lib/radiator/podcast.ex index c121569b..6e148fc4 100644 --- a/lib/radiator/podcast.ex +++ b/lib/radiator/podcast.ex @@ -285,16 +285,60 @@ defmodule Radiator.Podcast do end @doc """ - Returns the list of episodes. + Returns the query for list of episodes exluding the once that are marked as deleted. ## Examples - iex> list_episodes() + iex> list_available_episodes_query() + %Ecto.Query{} + + """ + def list_available_episodes_query do + from e in Episode, + where: [is_deleted: false], + order_by: [desc: e.number] + end + + @doc """ + Returns the query for list of episodes including the (soft) deleted once. + + ## Examples + + iex> list_all_episodes_query() + %Ecto.Query{} + + """ + def list_all_episodes_query do + from e in Episode, + order_by: [desc: e.number] + end + + @doc """ + Returns the list of episodes exluding the once that are marked as deleted. + + ## Examples + + iex> list_available_episodes() [%Episode{}, ...] """ - def list_episodes do - Repo.all(Episode) + def list_available_episodes do + list_available_episodes_query() + |> Repo.all() + end + + @doc """ + Returns the list of episodes including the (soft) deleted once. + + ## Examples + + iex> list_all_episodes() + [%Episode{}, ...] + + """ + def list_all_episodes do + list_all_episodes_query() + |> Repo.all() end @doc """ @@ -330,7 +374,10 @@ defmodule Radiator.Podcast do def get_current_episode_for_show(show_id) do Repo.one( - from e in Episode, where: e.show_id == ^show_id, order_by: [desc: e.number], limit: 1 + from e in Episode, + where: [show_id: ^show_id, is_deleted: false], + order_by: [desc: e.number], + limit: 1 ) end @@ -356,7 +403,7 @@ defmodule Radiator.Podcast do query = from e in Episode, select: max(e.number), - where: [show_id: ^show_id] + where: [show_id: ^show_id, is_deleted: false] max_number = Repo.one(query) || 0 max_number + 1 @@ -393,7 +440,9 @@ defmodule Radiator.Podcast do """ def delete_episode(%Episode{} = episode) do - Repo.delete(episode) + episode + |> Episode.changeset(%{is_deleted: true}) + |> Repo.update() end @doc """ diff --git a/lib/radiator/podcast/episode.ex b/lib/radiator/podcast/episode.ex index bbaa54d1..845d6c58 100644 --- a/lib/radiator/podcast/episode.ex +++ b/lib/radiator/podcast/episode.ex @@ -14,6 +14,8 @@ defmodule Radiator.Podcast.Episode do field :number, :integer field :publish_date, :date field :slug, :string + field :is_deleted, :boolean, default: false + field :deleted_at, :utc_datetime belongs_to :show, Show @@ -23,10 +25,11 @@ defmodule Radiator.Podcast.Episode do @doc false def changeset(episode, attrs) do episode - |> cast(attrs, [:title, :show_id, :number, :publish_date, :slug]) + |> cast(attrs, [:title, :show_id, :number, :publish_date, :slug, :is_deleted, :deleted_at]) |> validate_required([:title, :show_id, :number]) |> validate_length(:title, min: 3) |> maybe_update_slug() + |> validate_deleted_at() end defp maybe_update_slug(changeset) do @@ -43,4 +46,19 @@ defmodule Radiator.Podcast.Episode do |> put_change(:slug, new_slug) end end + + defp validate_deleted_at(changeset) do + # Check if the is_deleted has changed + case get_change(changeset, :is_deleted) do + true -> + now = DateTime.utc_now() |> DateTime.truncate(:second) + put_change(changeset, :deleted_at, now) + + false -> + put_change(changeset, :deleted_at, nil) + + nil -> + changeset + end + end end diff --git a/lib/radiator_web/live/episode_live/index.ex b/lib/radiator_web/live/episode_live/index.ex index cc5be809..113ec1d2 100644 --- a/lib/radiator_web/live/episode_live/index.ex +++ b/lib/radiator_web/live/episode_live/index.ex @@ -17,7 +17,9 @@ defmodule RadiatorWeb.EpisodeLive.Index do @impl true def mount(%{"show" => show_id} = params, _session, socket) do - show = Podcast.get_show!(show_id, preload: :episodes) + show = + Podcast.get_show!(show_id, preload: [episodes: Podcast.list_available_episodes_query()]) + episode = get_selected_episode(params) socket @@ -62,6 +64,10 @@ defmodule RadiatorWeb.EpisodeLive.Index do save_episode(socket, socket.assigns.live_action, params) end + def handle_event("delete", params, socket) do + delete_episode(socket, params) + end + @impl true def handle_info(%NodeInsertedEvent{} = event, socket), do: proxy_event(event, socket) def handle_info(%NodeContentChangedEvent{} = event, socket), do: proxy_event(event, socket) @@ -120,7 +126,10 @@ defmodule RadiatorWeb.EpisodeLive.Index do "episode_id" => episode.id }) - show = Podcast.reload_assoc(socket.assigns.show, [:episodes]) + show = + Podcast.reload_assoc(socket.assigns.show, + episodes: Podcast.list_available_episodes_query() + ) socket |> assign(:show, show) @@ -137,7 +146,10 @@ defmodule RadiatorWeb.EpisodeLive.Index do defp save_episode(socket, :edit, params) do case Podcast.update_episode(socket.assigns.episode, params) do {:ok, episode} -> - show = Podcast.reload_assoc(socket.assigns.show, [:episodes]) + show = + Podcast.reload_assoc(socket.assigns.show, + episodes: Podcast.list_available_episodes_query() + ) socket |> assign(:show, show) @@ -151,6 +163,31 @@ defmodule RadiatorWeb.EpisodeLive.Index do end end + defp delete_episode(socket, %{"id" => id}) do + id + |> Podcast.get_episode!() + |> Podcast.delete_episode() + |> case do + {:ok, _episode} -> + show = + Podcast.reload_assoc(socket.assigns.show, + episodes: Podcast.list_available_episodes_query() + ) + + socket + |> assign(:show, show) + |> assign(:episodes, show.episodes) + |> put_flash(:info, "Episode deleted") + |> push_patch(to: ~p"/admin/podcast/#{socket.assigns.show}") + |> reply(:noreply) + + {:error, _changeset} -> + socket + |> put_flash(:error, "Failed to delete episode") + |> reply(:noreply) + end + end + defp get_selected_episode(%{"episode" => episode_id}) do Podcast.get_episode!(episode_id) end diff --git a/priv/repo/migrations/20240918192918_add_is_deleted_to_episodes.exs b/priv/repo/migrations/20240918192918_add_is_deleted_to_episodes.exs new file mode 100644 index 00000000..44ffcbc6 --- /dev/null +++ b/priv/repo/migrations/20240918192918_add_is_deleted_to_episodes.exs @@ -0,0 +1,10 @@ +defmodule Radiator.Repo.Migrations.AddIsDeletedToEpisodes do + use Ecto.Migration + + def change do + alter table(:episodes) do + add :is_deleted, :boolean, default: false + add :deleted_at, :utc_datetime + end + end +end diff --git a/test/radiator/podcast_test.exs b/test/radiator/podcast_test.exs index fa1671c4..92f8b28a 100644 --- a/test/radiator/podcast_test.exs +++ b/test/radiator/podcast_test.exs @@ -156,9 +156,27 @@ defmodule Radiator.PodcastTest do describe "episodes" do @invalid_attrs %{title: nil} - test "list_episodes/0 returns all episodes" do + test "list_all_episodes/0 returns all episodes" do + deleted_episode = + episode_fixture( + is_deleted: true, + deleted_at: DateTime.utc_now() |> DateTime.truncate(:second) + ) + + assert Podcast.list_all_episodes() == [deleted_episode] + end + + test "list_available_episodes/0 returns all episodes" do episode = episode_fixture() - assert Podcast.list_episodes() == [episode] + + _deleted_episode = + episode_fixture( + is_deleted: true, + deleted_at: DateTime.utc_now() |> DateTime.truncate(:second) + ) + + found_episodes = Podcast.list_available_episodes() + assert found_episodes == [episode] end test "get_episode!/1 returns the episode with given id" do @@ -201,7 +219,11 @@ defmodule Radiator.PodcastTest do test "delete_episode/1 deletes the episode" do episode = episode_fixture() assert {:ok, %Episode{}} = Podcast.delete_episode(episode) - assert_raise Ecto.NoResultsError, fn -> Podcast.get_episode!(episode.id) end + episode = Podcast.get_episode!(episode.id) + assert episode.is_deleted == true + assert episode.deleted_at != nil + + assert Podcast.list_available_episodes() == [] end test "change_episode/1 returns a episode changeset" do From 976c1e0a6a17e3890e4879baf61108747bc1a9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Wo=CC=88ginger?= Date: Tue, 8 Oct 2024 11:35:17 +0200 Subject: [PATCH 2/4] filter ContentChangeEvents: process only events from other sockets --- lib/radiator_web/live/outline_component.ex | 6 ++++++ test/radiator_web/live/outline_live_test.exs | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/radiator_web/live/outline_component.ex b/lib/radiator_web/live/outline_component.ex index 27e2ccbe..2fb678b0 100644 --- a/lib/radiator_web/live/outline_component.ex +++ b/lib/radiator_web/live/outline_component.ex @@ -35,6 +35,12 @@ defmodule RadiatorWeb.OutlineComponent do |> reply(:ok) end + def update( + %{event: %NodeContentChangedEvent{event_id: <<_::binary-size(36)>> <> ":" <> id}}, + %{id: id} = socket + ), + do: reply(socket, :ok) + def update(%{event: %NodeContentChangedEvent{node_id: node_id, content: content}}, socket) do socket |> push_event("set_content", %{uuid: node_id, content: content}) diff --git a/test/radiator_web/live/outline_live_test.exs b/test/radiator_web/live/outline_live_test.exs index 2038c6c8..ff2f4e95 100644 --- a/test/radiator_web/live/outline_live_test.exs +++ b/test/radiator_web/live/outline_live_test.exs @@ -76,7 +76,6 @@ defmodule RadiatorWeb.OutlineLiveTest do keep_liveview_alive() - assert_push_event(live, "set_content", %{uuid: ^uuid, content: "node_1_updated"}) assert_push_event(other_live, "set_content", %{uuid: ^uuid, content: "node_1_updated"}) end @@ -90,7 +89,6 @@ defmodule RadiatorWeb.OutlineLiveTest do keep_liveview_alive() - assert_push_event(live, "set_content", %{uuid: ^uuid, content: "no"}) assert_push_event(other_live, "set_content", %{uuid: ^uuid, content: "no"}) assert live |> has_element?("[value=de_2]") From 77a56c4238ca2e930781f77e1f47e3342886e20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Tue, 8 Oct 2024 13:11:43 +0200 Subject: [PATCH 3/4] a bit less space for shortcuts --- .../components/outline_components.ex | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/lib/radiator_web/components/outline_components.ex b/lib/radiator_web/components/outline_components.ex index 2ec3d9be..1ed16c4f 100644 --- a/lib/radiator_web/components/outline_components.ex +++ b/lib/radiator_web/components/outline_components.ex @@ -63,52 +63,46 @@ defmodule RadiatorWeb.OutlineComponents do """ From e771a6e56653c010332a64fb1285e637961da19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Tue, 8 Oct 2024 13:35:00 +0200 Subject: [PATCH 4/4] fix flaky test --- test/radiator/outline/node_repository_test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/radiator/outline/node_repository_test.exs b/test/radiator/outline/node_repository_test.exs index 4cbfc03e..23b55995 100644 --- a/test/radiator/outline/node_repository_test.exs +++ b/test/radiator/outline/node_repository_test.exs @@ -119,7 +119,10 @@ defmodule Radiator.Outline.NodeRepositoryTest do nested_node_1: nested_node_1, nested_node_2: nested_node_2 } do - assert NodeRepository.get_all_siblings(node_3) == [nested_node_1, nested_node_2] + all_siblings = NodeRepository.get_all_siblings(node_3) + assert 2 = Enum.count(all_siblings) + assert Enum.member?(all_siblings, nested_node_1) + assert Enum.member?(all_siblings, nested_node_2) end test "does not return child nodes deeper then 1 level", %{