diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 81e8d5b4..cc092b45 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -78,9 +78,9 @@ defmodule Radiator.Outline do # if no previous node is given, the new node will be inserted as the first child of the parent node def insert_node(%{"show_id" => _show_id} = attrs) do Repo.transaction(fn -> - prev_id = attrs["prev_id"] - parent_id = attrs["parent_id"] episode_id = attrs["episode_id"] + prev_id = attrs["prev_id"] + parent_id = convert_parent_id_to_intern(attrs["parent_id"], episode_id) prev_node = NodeRepository.get_node_if(prev_id) parent_node = find_parent_node(prev_node, parent_id) @@ -116,6 +116,13 @@ defmodule Radiator.Outline do |> insert_node() end + defp convert_parent_id_to_intern(nil, episode_id) do + {episode_root, _} = NodeRepository.get_virtual_nodes_for_episode(episode_id) + episode_root.uuid + end + + defp convert_parent_id_to_intern(parent_id, _episode_id), do: parent_id + @doc """ Intends a node given by its id (by using the tab key). @@ -241,6 +248,12 @@ defmodule Radiator.Outline do {:error, :parent_and_prev_not_consistent} end + def move_node(node_id, prev_id: new_prev_id, parent_id: nil) do + node = NodeRepository.get_node(node_id) + intern_parent = convert_parent_id_to_intern(nil, node.episode_id) + move_node(node_id, prev_id: new_prev_id, parent_id: intern_parent) + end + def move_node(node_id, prev_id: new_prev_id, parent_id: new_parent_id) do case NodeRepository.get_node(node_id) do nil -> @@ -482,10 +495,14 @@ defmodule Radiator.Outline do Repo.transaction(fn -> old_next_node = - NodeRepository.get_node_by_parent_and_prev(get_node_id(parent_node), node.uuid) + NodeRepository.get_node_by_parent_and_prev( + get_node_id(parent_node), + node.uuid, + node.episode_id + ) new_next_node = - NodeRepository.get_node_by_parent_and_prev(new_parent_id, new_prev_id) + NodeRepository.get_node_by_parent_and_prev(new_parent_id, new_prev_id, node.episode_id) {:ok, node} = NodeRepository.move_node_if(node, new_parent_id, new_prev_id) diff --git a/lib/radiator/outline/node_repository.ex b/lib/radiator/outline/node_repository.ex index fc12c84d..94049c40 100644 --- a/lib/radiator/outline/node_repository.ex +++ b/lib/radiator/outline/node_repository.ex @@ -26,6 +26,95 @@ defmodule Radiator.Outline.NodeRepository do |> Repo.insert() end + @doc """ + Creates the internal nodes for a show, this is the global root + and the global inbox. + """ + def create_virtual_nodes_for_show(show_id) do + # create a root node for a show + {:ok, show_root} = + create_node(%{ + show_id: show_id, + parent_id: nil, + prev_id: nil, + _type: "global_root" + }) + + {:ok, global_inbox} = + create_node(%{ + show_id: show_id, + parent_id: show_root.uuid, + prev_id: nil, + _type: "global_inbox" + }) + + {show_root, global_inbox} + end + + @doc """ + Creates the internal nodes for an episode, this is the episode root + and the episode inbox. + """ + def create_virtual_nodes_for_episode(%{id: episode_id, show_id: show_id}) do + # create a root node for a show + {show_root, _global_inbox} = get_virtual_nodes_for_show(show_id) + + {:ok, episode_root} = + create_node(%{ + episode_id: episode_id, + show_id: show_id, + parent_id: show_root.uuid, + prev_id: nil, + _type: "episode_root" + }) + + {:ok, episode_inbox} = + create_node(%{ + episode_id: episode_id, + show_id: show_id, + parent_id: episode_root.uuid, + prev_id: nil, + _type: "episode_inbox" + }) + + {episode_root, episode_inbox} + end + + @doc """ + returns the virtual nodes of a show + TODO: perhaps these should be stored in the show itself? + """ + def get_virtual_nodes_for_show(show_id) do + [node_1, node_2] = + Node + |> where([p], p.show_id == ^show_id) + |> where([p], p._type in [:global_root, :global_inbox]) + |> Repo.all() + + if node_1._type == :global_root do + {node_1, node_2} + else + {node_2, node_1} + end + end + + @doc """ + TODO add documentation + """ + def get_virtual_nodes_for_episode(episode_id) do + [node_1, node_2] = + Node + |> where([p], p.episode_id == ^episode_id) + |> where([p], p._type in [:episode_root, :episode_inbox]) + |> Repo.all() + + if node_1._type == :episode_root do + {node_1, node_2} + else + {node_2, node_1} + end + end + @doc """ Deletes a node from the repository. @@ -62,7 +151,7 @@ defmodule Radiator.Outline.NodeRepository do ## Examples - iex> list_nodes(123) + iex> list_nodes_by_episode(123) [%Node{}, ...] """ @@ -76,6 +165,25 @@ defmodule Radiator.Outline.NodeRepository do |> List.flatten() end + @doc """ + Returns the list of nodes for a show. + + ## Examples + + iex> _list_nodes_by_show(123) + [%Node{}, ...] + + """ + + def _list_nodes_by_show(show_id) do + Node + |> where([p], p.show_id == ^show_id) + |> Repo.all() + # |> Enum.group_by(& &1.parent_id) + # |> Enum.map(fn {_parent_id, children} -> Radiator.Outline.order_sibling_nodes(children) end) + |> List.flatten() + end + @doc """ Returns the the number of nodes for an episode @@ -150,17 +258,20 @@ defmodule Radiator.Outline.NodeRepository do end @doc """ - Gets a single node defined by the given prev_id and parent_id. + Gets a single node defined by the given prev_id and parent_id and the + episode id. + Returns `nil` if the Node cannot be found. ## Examples - iex> get_node_by_parent_and_prev("5adf3b360fb0", "380d56cf") + iex> get_node_by_parent_and_prev("5adf3b360fb0", "380d56cf", 23) nil - iex> get_node_by_parent_and_prev("5e3f5a0422a4", "b78a976d") + iex> get_node_by_parent_and_prev("5e3f5a0422a4", "b78a976d", 23) %Node{uuid: "33b2a1dac9b1", parent_id: "5e3f5a0422a4", prev_id: "b78a976d"} """ - def get_node_by_parent_and_prev(parent_id, prev_id) do + def get_node_by_parent_and_prev(parent_id, prev_id, episode_id) do Node + |> where(episode_id: ^episode_id) |> where_prev_node_equals(prev_id) |> where_parent_node_equals(parent_id) |> Repo.one() @@ -273,6 +384,17 @@ defmodule Radiator.Outline.NodeRepository do |> Repo.one() end + # @TODO needed? + def get_root_node_for_show(show_id) do + Node + |> where([p], p.show_id == ^show_id) + # |> where([p], is_nil(p.episode_id)) + # |> where([p], is_nil(p.parent_id)) + # |> where([p], is_nil(p.prev_id)) + |> where([p], p._type == :global_root) + |> Repo.one() + end + @doc """ Returns all direct child nodes of a given node. ## Examples diff --git a/lib/radiator/podcast.ex b/lib/radiator/podcast.ex index e840eaaa..28f6a344 100644 --- a/lib/radiator/podcast.ex +++ b/lib/radiator/podcast.ex @@ -7,6 +7,7 @@ defmodule Radiator.Podcast do import Ecto.Query, warn: false alias Radiator.Repo + alias Radiator.Outline.NodeRepository alias Radiator.Podcast.{Episode, Network, Show, ShowHosts} @doc """ @@ -169,6 +170,47 @@ defmodule Radiator.Podcast do |> Repo.preload(preload) end + @doc """ + Gets a single show and preloads its virtual nodes (root and inbox) in a single query. + Sets the virtual attributes root_node_id and inbox_node_id. + + ## Examples + + iex> get_show(123) + %Show{root_node_id: "uuid-1", inbox_node_id: "uuid-2"} + + iex> get_show(456) + nil + + """ + def get_show(id, preload: preload) do + Show + |> join(:inner, [s], n in Node, + on: n.show_id == s.id and n._type in [:global_root, :global_inbox]) + |> where([s, _], s.id == ^id) + |> select([s, n], %{ + show: s, + nodes: n + }) + |> Repo.all() + |> Repo.preload(preload) # FIXME + |> case do + [] -> + nil + nodes_and_show -> + show = hd(nodes_and_show).show + {root_id, inbox_id} = nodes_and_show + |> Enum.map(& &1.nodes) + |> Enum.reduce({nil, nil}, fn node, {root, inbox} -> + case node._type do + :global_root -> {node.uuid, inbox} + :global_inbox -> {root, node.uuid} + end + end) + %{show | root_node_id: root_id, inbox_node_id: inbox_id} + end + end + @doc """ Creates a show. @@ -182,9 +224,17 @@ defmodule Radiator.Podcast do """ def create_show(attrs \\ %{}) do - %Show{} - |> Show.changeset(attrs) - |> Repo.insert() + # also need to create the nodes for the show + # start a transaction = + Repo.transaction(fn -> + {:ok, show} = + %Show{} + |> Show.changeset(attrs) + |> Repo.insert() + + {_show_root, _global_inbox} = NodeRepository.create_virtual_nodes_for_show(show.id) + show + end) end @doc """ @@ -380,6 +430,15 @@ defmodule Radiator.Podcast do order_by: [desc: e.number] end + @doc """ + Lists all episodes for a given show. + """ + def list_episodes_for_show(show_id) do + list_available_episodes_query() + |> where([e], e.show_id == ^show_id) + |> Repo.all() + end + @doc """ Returns the query for list of episodes including the (soft) deleted once. diff --git a/lib/radiator/podcast/episode.ex b/lib/radiator/podcast/episode.ex index 845d6c58..ea6a6176 100644 --- a/lib/radiator/podcast/episode.ex +++ b/lib/radiator/podcast/episode.ex @@ -16,6 +16,8 @@ defmodule Radiator.Podcast.Episode do field :slug, :string field :is_deleted, :boolean, default: false field :deleted_at, :utc_datetime + field :root_node_id, :binary_id, virtual: true + field :inbox_node_id, :binary_id, virtual: true belongs_to :show, Show diff --git a/lib/radiator/podcast/show.ex b/lib/radiator/podcast/show.ex index 1bd5fe3c..747e2b94 100644 --- a/lib/radiator/podcast/show.ex +++ b/lib/radiator/podcast/show.ex @@ -12,9 +12,10 @@ defmodule Radiator.Podcast.Show do schema "shows" do field :title, :string field :description, :string + field :root_node_id, :binary_id, virtual: true + field :inbox_node_id, :binary_id, virtual: true belongs_to :network, Network - has_many(:episodes, Episode) has_many(:outline_nodes, Node) many_to_many(:hosts, User, join_through: "show_hosts") diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index f779068f..46873173 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -264,6 +264,23 @@ defmodule Radiator.OutlineTest do assert NodeRepository.get_node!(nested_node_2.uuid).prev_id == nested_node_1.uuid end + test "when parent node is nil it will be set to the episode root", %{ + parent_node: parent_node + } do + node_attrs = %{"parent_id" => nil, "episode_id" => parent_node.episode_id} + {:ok, %{node: new_node}} = Outline.insert_node(node_attrs) + # do not test result since it will be altered + # but fetching if from the repo with low level functions will return the true value + {episode_root, _} = NodeRepository.get_virtual_nodes_for_episode(parent_node.episode_id) + assert NodeRepository.get_node!(new_node.uuid).parent_id == episode_root.uuid + end + + test "when parent node is not given it will be set to the episode root", %{ + parent_node: parent_node + } do + + end + test "without a parent node the inserted node will be put at the top", %{ parent_node: parent_node } do diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 208f87df..b3b1b409 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -16,6 +16,7 @@ defmodule Radiator.DataCase do use ExUnit.CaseTemplate alias Ecto.Adapters.SQL.Sandbox + alias Radiator.Outline.NodeRepository alias Radiator.PodcastFixtures import Radiator.OutlineFixtures @@ -93,7 +94,10 @@ defmodule Radiator.DataCase do end def simple_node_fixture_hierachical(_) do - episode = PodcastFixtures.episode_fixture() + show = PodcastFixtures.show_fixture() + {_show_root, _global_inbox} = NodeRepository.create_virtual_nodes_for_show(show.id) + + episode = PodcastFixtures.episode_fixture(show_id: show.id) node_1 = node_fixture( @@ -120,13 +124,17 @@ defmodule Radiator.DataCase do end def complex_node_fixture(_) do + show = PodcastFixtures.show_fixture() + {_show_root, _global_inbox} = NodeRepository.create_virtual_nodes_for_show(show.id) episode = PodcastFixtures.episode_fixture() + {episode_root, _eposide_inbox} = NodeRepository.create_virtual_nodes_for_episode(episode) + parent_node = node_fixture( episode_id: episode.id, show_id: episode.show_id, - parent_id: nil, + parent_id: episode_root.uuid, prev_id: nil, content: "root of all evil" ) diff --git a/test/support/fixtures/podcast_fixtures.ex b/test/support/fixtures/podcast_fixtures.ex index 20666a54..b04ff1fe 100644 --- a/test/support/fixtures/podcast_fixtures.ex +++ b/test/support/fixtures/podcast_fixtures.ex @@ -23,7 +23,7 @@ defmodule Radiator.PodcastFixtures do Generate a show. """ def show_fixture(attrs \\ %{}, hosts \\ []) do - network = get_network(attrs) + network = extract_network(attrs) {:ok, show} = attrs @@ -40,7 +40,7 @@ defmodule Radiator.PodcastFixtures do Generate a episode. """ def episode_fixture(attrs \\ %{}) do - show = get_show(attrs) + show = extract_show(attrs) number = Podcast.get_next_episode_number(show.id) {:ok, episode} = @@ -55,9 +55,9 @@ defmodule Radiator.PodcastFixtures do episode end - defp get_network(%{network_id: id}), do: Podcast.get_network!(id) - defp get_network(_), do: network_fixture() + defp extract_network(%{network_id: id}), do: Podcast.get_network!(id) + defp extract_network(_), do: network_fixture() - defp get_show(%{show_id: id}), do: Podcast.get_show!(id) - defp get_show(_), do: show_fixture() + defp extract_show(%{show_id: id}), do: Podcast.get_show!(id) + defp extract_show(_), do: show_fixture() end