Skip to content

Commit

Permalink
enhance delete function to keep the tree
Browse files Browse the repository at this point in the history
in a consistent state

fix related to #515
  • Loading branch information
electronicbites committed Feb 25, 2024
1 parent 1bd0a00 commit 598d2cf
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 5 deletions.
60 changes: 60 additions & 0 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,39 @@ defmodule Radiator.Outline do
|> Repo.get!(id)
end

@doc """
Returns the previous node of a given node in the outline tree.
Returns `nil` if prev_id of the node is nil.
## Examples
iex> get_prev_node(%Node{prev_id: nil})
nil
iex> get_prev_node(%Node{prev_id: 42})
%Node{uuid: 42}
"""
def get_prev_node(node) when is_nil(node.prev_id), do: nil

def get_prev_node(node) do
Node
|> where([n], n.uuid == ^node.prev_id)
|> Repo.one()
end

@doc """
Returns all child nodes of a given node.
## Examples
iex> get_all_child_nodes(%Node{})
[%Node{}, %Node{}]
"""
def get_all_child_nodes(node) do
Node
|> where([n], n.parent_id == ^node.uuid)
|> Repo.all()
end

@doc """
Gets a single node.
Expand Down Expand Up @@ -218,6 +251,33 @@ defmodule Radiator.Outline do
"""
def delete_node(%Node{} = node) do
next_node =
Node
|> where([n], n.prev_id == ^node.uuid)
|> Repo.one()

prev_node = get_prev_node(node)

prev_uuid =
if prev_node do
prev_node.uuid
else
nil
end

if next_node do
next_node
|> Node.move_node_changeset(%{prev_id: prev_uuid})
|> Repo.update()
end

# no tail recursion but we dont have too much levels in a tree
node
|> get_all_child_nodes()
|> Enum.each(fn child_node ->
delete_node(child_node)
end)

node
|> Repo.delete()
|> broadcast_node_action(:delete)
Expand Down
5 changes: 5 additions & 0 deletions lib/radiator/outline/node.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ defmodule Radiator.Outline.Node do
|> validate_required([:content])
end

def move_node_changeset(node, attrs) do
node
|> cast(attrs, [:parent_id, :prev_id])
end

defp trim(content) when is_binary(content), do: String.trim(content)
defp trim(content), do: content
end
117 changes: 112 additions & 5 deletions test/radiator/outline_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ defmodule Radiator.OutlineTest do
alias Radiator.Outline
alias Radiator.Outline.Node
alias Radiator.PodcastFixtures
alias Radiator.Repo

import Radiator.OutlineFixtures
import Ecto.Query, warn: false
Expand Down Expand Up @@ -83,12 +82,123 @@ defmodule Radiator.OutlineTest do
end
end

describe "get_prev_node/1" do
setup :complex_node_fixture

test "returns the previous node", %{node_2: node_2, node_3: node_3} do
assert Outline.get_prev_node(node_3) == node_2
end

test "returns nil if there is no previous node", %{node_1: node_1} do
assert Outline.get_prev_node(node_1) == nil
end
end

describe "get_all_child_nodes/1" do
setup :complex_node_fixture

test "returns all child nodes", %{node_3: node_3, nested_node_1: nested_node_1, nested_node_2: nested_node_2} do
assert Outline.get_all_child_nodes(node_3) == [nested_node_1, nested_node_2]
end

test "returns an empty list if there are no child nodes", %{node_1: node_1} do
assert Outline.get_all_child_nodes(node_1) == []
end
end

describe "delete_node/1" do
setup :complex_node_fixture

test "deletes the node" do
node = node_fixture()
assert {:ok, %Node{}} = Outline.delete_node(node)
assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node.uuid) end
end

test "next node must be updated", %{
node_2: node_2,
node_3: node_3,
node_4: node_4
} do
assert node_4.prev_id == node_3.uuid

assert {:ok, %Node{}} = Outline.delete_node(node_3)
# reload nodes
node_4 = Outline.get_node!(node_4.uuid)
node_2 = Outline.get_node!(node_2.uuid)

assert node_4.prev_id == node_2.uuid
end

test "works for last element in list", %{
node_6: node_6
} do
episode_id = node_6.episode_id

count_nodes =
episode_id
|> Outline.list_nodes_by_episode()
|> Enum.count()

assert {:ok, %Node{}} = Outline.delete_node(node_6)

new_count_nodes =
episode_id
|> Outline.list_nodes_by_episode()
|> Enum.count()

assert new_count_nodes == count_nodes - 1
end

test "works for first element in list", %{
node_1: node_1,
node_2: node_2
} do
episode_id = node_1.episode_id

count_nodes =
episode_id
|> Outline.list_nodes_by_episode()
|> Enum.count()

assert {:ok, %Node{}} = Outline.delete_node(node_1)

new_count_nodes =
episode_id
|> Outline.list_nodes_by_episode()
|> Enum.count()

assert new_count_nodes == count_nodes - 1
node_2 = Outline.get_node!(node_2.uuid)
assert node_2.prev_id == nil
end

test "delete also child elements", %{
node_3: node_3,
nested_node_1: nested_node_1,
nested_node_2: nested_node_2
} do
assert {:ok, %Node{}} = Outline.delete_node(node_3)

assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(nested_node_1.uuid) end
assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(nested_node_2.uuid) end
end

test "when top parent gets deleted the whole tree will be gone", %{
node_1: node_1,
node_4: node_4,
node_6: node_6,
nested_node_2: nested_node_2,
parent: parent
} do
assert {:ok, %Node{}} = Outline.delete_node(parent)

# test some of elements in the tree
assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node_1.uuid) end
assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node_4.uuid) end
assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node_6.uuid) end
assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(nested_node_2.uuid) end
end
end

describe "get_node_tree/1" do
Expand All @@ -98,10 +208,7 @@ defmodule Radiator.OutlineTest do
episode_id = parent.episode_id
assert {:ok, tree} = Outline.get_node_tree(episode_id)

all_nodes =
Node
|> where([n], n.episode_id == ^episode_id)
|> Repo.all()
all_nodes = Outline.list_nodes_by_episode(episode_id)

assert Enum.count(tree) == Enum.count(all_nodes)

Expand Down

0 comments on commit 598d2cf

Please sign in to comment.