Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add node tree function #514

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 101 additions & 19 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,88 @@ defmodule Radiator.Outline do
|> Repo.get(id)
end

@doc """
Gets all nodes of an episode as a tree.
Uses a Common Table Expression (CTE) to recursively query the database.
Sets the level of each node in the tree. Level 0 are the root nodes (without a parent)
Returns a list with all nodes of the episode sorted by the level.
## Examples

iex> get_node_tree(123)
[%Node{}, %Node{}, ..]

SQL:
WITH RECURSIVE node_tree AS (
SELECT uuid, content, parent_id, prev_id, 0 AS level
FROM outline_nodes
WHERE episode_id = ?::integer and parent_id is NULL
UNION ALL
SELECT outline_nodes.uuid, outline_nodes.content, outline_nodes.parent_id, outline_nodes.prev_id, node_tree.level + 1
FROM outline_nodes
JOIN node_tree ON outline_nodes.parent_id = node_tree.uuid
)
SELECT * FROM node_tree;
"""
def get_node_tree(episode_id) do
node_tree_initial_query =
Node
|> where([n], is_nil(n.parent_id))
|> where([n], n.episode_id == ^episode_id)
|> select([n], %{
uuid: n.uuid,
content: n.content,
parent_id: n.parent_id,
prev_id: n.prev_id,
level: 0
})

node_tree_recursion_query =
from outline_node in "outline_nodes",
join: node_tree in "node_tree",
on: outline_node.parent_id == node_tree.uuid,
select: [
outline_node.uuid,
outline_node.content,
outline_node.parent_id,
outline_node.prev_id,
node_tree.level + 1
]

node_tree_query =
node_tree_initial_query
|> union_all(^node_tree_recursion_query)

tree =
"node_tree"
|> recursive_ctes(true)
|> with_cte("node_tree", as: ^node_tree_query)
|> select([n], %{
uuid: n.uuid,
content: n.content,
parent_id: n.parent_id,
prev_id: n.prev_id,
level: n.level
})
|> Repo.all()
|> Enum.map(fn %{
uuid: uuid,
content: content,
parent_id: parent_id,
prev_id: prev_id,
level: level
} ->
%Node{
uuid: binaray_uuid_to_ecto_uuid(uuid),
content: content,
parent_id: binaray_uuid_to_ecto_uuid(parent_id),
prev_id: binaray_uuid_to_ecto_uuid(prev_id),
level: level
}
end)

{:ok, tree}
end

@doc """
Creates a node.

Expand All @@ -92,26 +174,33 @@ defmodule Radiator.Outline do
"""
def create_node(attrs \\ %{}) do
%Node{}
|> Node.changeset(attrs)
|> Node.insert_changeset(attrs)
|> Repo.insert()
|> broadcast_node_action(:insert)
end

def create_node(attrs, %{id: id}) do
%Node{creator_id: id}
|> Node.insert_changeset(attrs)
|> Repo.insert()
|> broadcast_node_action(:insert)
end

@doc """
Updates a node.
Updates a nodes content.

## Examples

iex> update_node(node, %{field: new_value})
iex> update_node_content(node, %{content: new_value})
{:ok, %Node{}}

iex> update_node(node, %{field: bad_value})
iex> update_node_content(node, %{content: nil})
{:error, %Ecto.Changeset{}}

"""
def update_node(%Node{} = node, attrs) do
def update_node_content(%Node{} = node, attrs) do
node
|> Node.changeset(attrs)
|> Node.update_content_changeset(attrs)
|> Repo.update()
|> broadcast_node_action(:update)
end
Expand All @@ -134,23 +223,16 @@ defmodule Radiator.Outline do
|> broadcast_node_action(:delete)
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking node changes.

## Examples

iex> change_node(node)
%Ecto.Changeset{data: %Node{}}

"""
def change_node(%Node{} = node, attrs \\ %{}) do
Node.changeset(node, attrs)
end

defp broadcast_node_action({:ok, node}, action) do
PubSub.broadcast(Radiator.PubSub, @topic, {action, node})
{:ok, node}
end

defp broadcast_node_action({:error, error}, _action), do: {:error, error}

defp binaray_uuid_to_ecto_uuid(nil), do: nil

defp binaray_uuid_to_ecto_uuid(uuid) do
Ecto.UUID.load!(uuid)
end
end
37 changes: 21 additions & 16 deletions lib/radiator/outline/node.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,36 @@ defmodule Radiator.Outline.Node do
field :creator_id, :integer
field :parent_id, Ecto.UUID
field :prev_id, Ecto.UUID
field :level, :integer, virtual: true

belongs_to :episode, Episode

timestamps(type: :utc_datetime)
end

@required_fields [
:episode_id
]

@optional_fields [
:content,
:creator_id,
:parent_id,
:prev_id
]

@all_fields @optional_fields ++ @required_fields
@doc """
A changeset for inserting a new node
Work in progress. Since we currently ignore the tree structure, there is
no concept for a root node.
Also questionable wether a node really needs a content from beginning. So probably a root
doesnt have a content
Another issue might be we need to create the uuid upfront and pass it here
"""
def insert_changeset(node, attributes) do
node
|> cast(attributes, [:content, :episode_id, :creator_id, :parent_id, :prev_id])
|> update_change(:content, &trim/1)
|> validate_required([:content, :episode_id])
end

@doc false
def changeset(node, attrs) do
@doc """
Changeset for updating the content of a node
"""
def update_content_changeset(node, attrs) do
node
|> cast(attrs, @all_fields)
|> cast(attrs, [:content])
|> update_change(:content, &trim/1)
|> validate_required(@required_fields)
|> validate_required([:content])
end

defp trim(content) when is_binary(content), do: String.trim(content)
Expand Down
2 changes: 1 addition & 1 deletion lib/radiator_web/live/episode_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ defmodule RadiatorWeb.EpisodeLive.Index do

case Outline.get_node(uuid) do
nil -> nil
node -> Outline.update_node(node, attrs)
node -> Outline.update_node_content(node, attrs)
end

socket
Expand Down
5 changes: 4 additions & 1 deletion priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ alias Radiator.{Accounts, Outline, Podcast}
{:ok, show} =
Podcast.create_show(%{title: "Tech Weekly", network_id: network.id})

{:ok, _episode} =
{:ok, past_episode} =
Podcast.create_episode(%{title: "past episode", show_id: show.id})

{:ok, current_episode} =
Expand Down Expand Up @@ -60,3 +60,6 @@ alias Radiator.{Accounts, Outline, Podcast}
episode_id: current_episode.id,
prev_id: node211.uuid
})

{:ok, past_parent_node} =
Outline.create_node(%{content: "Old Content", episode_id: past_episode.id})
Loading