Skip to content

Commit

Permalink
Merge pull request #510 from podlove/connect-outline-nodes-to-episodes
Browse files Browse the repository at this point in the history
Connect outline nodes to episodes
  • Loading branch information
electronicbites committed Jan 14, 2024
2 parents 3864811 + c678a14 commit 787c2b1
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 31 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Podlove
Copyright (c) 2024 Podlove

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
6 changes: 5 additions & 1 deletion lib/radiator/outline/node.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Radiator.Outline.Node do
"""
use Ecto.Schema
import Ecto.Changeset
alias Radiator.Podcast.Episode

@derive {Jason.Encoder, only: [:uuid, :content, :creator_id, :parent_id, :prev_id]}

Expand All @@ -15,11 +16,14 @@ defmodule Radiator.Outline.Node do
field :parent_id, Ecto.UUID
field :prev_id, Ecto.UUID

belongs_to :episode, Episode

timestamps(type: :utc_datetime)
end

@required_fields [
:content
:content,
:episode_id
]

@optional_fields [
Expand Down
45 changes: 44 additions & 1 deletion lib/radiator/podcast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,27 @@ defmodule Radiator.Podcast do
"""
def get_episode!(id), do: Repo.get!(Episode, id)

@doc """
Finds the newest (TODO: not published ) episode for a show.
Returns %Episode{} or `nil` and expects an id of the show.
## Examples
iex> get_current_episode_for_show(123)
%Episode{}
iex> get_current_episode_for_show(456)
nil
"""
def get_current_episode_for_show(nil), do: nil

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
)
end

@doc """
Creates a episode.
Expand All @@ -239,11 +260,33 @@ defmodule Radiator.Podcast do
"""
def create_episode(attrs \\ %{}) do
attrs_with_number = set_number(attrs)

%Episode{}
|> Episode.changeset(attrs)
|> Episode.changeset(attrs_with_number)
|> Repo.insert()
end

defp set_number(%{number: _number} = attrs), do: attrs

defp set_number(%{show_id: show_id} = episode_attrs) do
number = get_highest_number(show_id) + 1
Map.put(episode_attrs, :number, number)
end

defp set_number(%{} = episode_attrs) do
Map.put(episode_attrs, :number, 0)
end

defp get_highest_number(show_id) do
query =
from e in Episode,
select: max(e.number),
where: [show_id: ^show_id]

Repo.one(query) || 0
end

@doc """
Updates a episode.
Expand Down
7 changes: 4 additions & 3 deletions lib/radiator/podcast/episode.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Radiator.Podcast.Episode do
@moduledoc """
Represents the Episode model.
TODO: Episodes should be numbered and ordered inside a show.
Episodes are numbered inside a show.
"""
use Ecto.Schema
import Ecto.Changeset
Expand All @@ -10,14 +10,15 @@ defmodule Radiator.Podcast.Episode do

schema "episodes" do
field :title, :string
field :number, :integer
belongs_to :show, Show
timestamps(type: :utc_datetime)
end

@doc false
def changeset(episode, attrs) do
episode
|> cast(attrs, [:title, :show_id])
|> validate_required([:title, :show_id])
|> cast(attrs, [:title, :show_id, :number])
|> validate_required([:title, :show_id, :number])
end
end
16 changes: 10 additions & 6 deletions lib/radiator_web/controllers/api/outline_controller.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
defmodule RadiatorWeb.Api.OutlineController do
use RadiatorWeb, :controller

alias Radiator.Accounts
alias Radiator.Outline
alias Radiator.{Accounts, Outline, Podcast}

def create(conn, %{"content" => content, "show_id" => show_id, "token" => token}) do
episode = Podcast.get_current_episode_for_show(show_id)

def create(conn, %{"content" => content, "token" => token}) do
{status_code, body} =
token
|> decode_token()
|> get_user_by_token()
|> create_node(content)
|> create_node(content, episode.id)
|> get_response()

conn
Expand All @@ -28,8 +29,11 @@ defmodule RadiatorWeb.Api.OutlineController do
defp get_user_by_token({:ok, token}), do: Accounts.get_user_by_api_token(token)
defp get_user_by_token(:error), do: {:error, :token}

defp create_node(nil, _), do: {:error, :user}
defp create_node(user, content), do: Outline.create_node(%{"content" => content}, user)
defp create_node(nil, _, _), do: {:error, :user}
defp create_node(_, _, nil), do: {:error, :episode}

defp create_node(user, content, episode_id),
do: Outline.create_node(%{"content" => content, "episode_id" => episode_id}, user)

defp get_response({:ok, node}), do: {200, %{uuid: node.uuid}}
defp get_response({:error, _}), do: {400, %{error: "params"}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Radiator.Repo.Migrations.AddOutlineReferenceToEpisode do
use Ecto.Migration

def change do
alter table(:outline_nodes) do
add :episode_id, references(:episodes, on_delete: :nothing)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Radiator.Repo.Migrations.AddNumberToEpisodes do
use Ecto.Migration

def change do
alter table(:episodes) do
add :number, :integer
end
end
end
14 changes: 7 additions & 7 deletions priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ alias Radiator.{Accounts, Outline, Podcast}
{:ok, _user_jim} =
Accounts.register_user(%{email: "[email protected]", password: "supersupersecret"})

{:ok, _node} =
Outline.create_node(%{content: "This is my first node"})

{:ok, _node} =
Outline.create_node(%{content: "Second node"})

{:ok, network} =
Podcast.create_network(%{title: "Podcast network"})

Expand All @@ -28,5 +22,11 @@ alias Radiator.{Accounts, Outline, Podcast}
{:ok, _episode} =
Podcast.create_episode(%{title: "past episode", show_id: show.id})

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

{:ok, _node} =
Outline.create_node(%{content: "This is my first node", episode_id: current_episode.id})

{:ok, _node} =
Outline.create_node(%{content: "Second node", episode_id: current_episode.id})
10 changes: 7 additions & 3 deletions test/radiator/outline_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Radiator.OutlineTest do
alias Radiator.Outline.Node

import Radiator.OutlineFixtures
alias Radiator.PodcastFixtures

@invalid_attrs %{content: nil}

Expand All @@ -21,22 +22,25 @@ defmodule Radiator.OutlineTest do
end

test "create_node/1 with valid data creates a node" do
valid_attrs = %{content: "some content"}
episode = PodcastFixtures.episode_fixture()
valid_attrs = %{content: "some content", episode_id: episode.id}

assert {:ok, %Node{} = node} = Outline.create_node(valid_attrs)
assert node.content == "some content"
end

test "create_node/1 trims whitespace from content" do
valid_attrs = %{content: " some content "}
episode = PodcastFixtures.episode_fixture()
valid_attrs = %{content: " some content ", episode_id: episode.id}

assert {:ok, %Node{} = node} = Outline.create_node(valid_attrs)
assert node.content == "some content"
end

test "create_node/1 can have a creator" do
episode = PodcastFixtures.episode_fixture()
user = %{id: 2}
valid_attrs = %{content: "some content"}
valid_attrs = %{content: "some content", episode_id: episode.id}

assert {:ok, %Node{} = node} = Outline.create_node(valid_attrs, user)
assert node.content == "some content"
Expand Down
48 changes: 48 additions & 0 deletions test/radiator/podcast_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,31 @@ defmodule Radiator.PodcastTest do
assert episode.show_id == show.id
end

test "create_episode/1 sets for first episode number 1" do
episode_attrs = %{title: "a new episode", show_id: show_fixture().id}

{:ok, %Episode{} = episode} = Podcast.create_episode(episode_attrs)
assert episode.number > 0
end

test "create_episode/1 finds the next highest number " do
show = show_fixture()
episode_fixture(show_id: show.id, number: 23)
episode_attrs = %{title: "my new episode", show_id: show.id}

{:ok, %Episode{} = episode} = Podcast.create_episode(episode_attrs)
assert episode.number == 24
end

test "create_episode/1 can be set explict" do
show = show_fixture()
episode_fixture(show_id: show.id, number: 2)
episode_attrs = %{title: "my new episode", number: 5, show_id: show.id}

{:ok, %Episode{} = episode} = Podcast.create_episode(episode_attrs)
assert episode.number == 5
end

test "create_episode/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Podcast.create_episode(@invalid_attrs)
end
Expand Down Expand Up @@ -162,5 +187,28 @@ defmodule Radiator.PodcastTest do
episode = episode_fixture()
assert %Ecto.Changeset{} = Podcast.change_episode(episode)
end

test "get_current_episode_for_show/1 returns nil when no show has been given" do
assert nil == Podcast.get_current_episode_for_show(nil)
end

test "get_current_episode_for_show/1 returns nil when no episode for show exists" do
show = show_fixture()
assert nil == Podcast.get_current_episode_for_show(show.id)
end

test "get_current_episode_for_show/1 returns episdoe for show" do
episode = episode_fixture()
assert episode == Podcast.get_current_episode_for_show(episode.show_id)
end

test "get_current_episode_for_show/1 returns the episode with the highest number" do
show = show_fixture()
# create new before old to ensure that the highest number is returned
# and not just the newest
episode_new = episode_fixture(number: 23, show_id: show.id)
_episode_old = episode_fixture(number: 22, show_id: show.id)
assert episode_new == Podcast.get_current_episode_for_show(show.id)
end
end
end
40 changes: 32 additions & 8 deletions test/radiator_web/controllers/api/outline_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,50 @@ defmodule RadiatorWeb.Api.OutlineControllerTest do
use RadiatorWeb.ConnCase, async: true

import Radiator.AccountsFixtures
import Radiator.PodcastFixtures

alias Radiator.Accounts
alias Radiator.Outline
alias Radiator.{Accounts, Outline, Podcast}

describe "POST /api/v1/outline" do
setup %{conn: conn} do
user = user_fixture()
show_id = episode_fixture().show_id

token =
user
|> Accounts.generate_user_api_token()
|> Base.url_encode64()

%{conn: conn, user: user, token: token}
%{conn: conn, user: user, token: token, show_id: show_id}
end

test "creates a node if content is present", %{conn: conn, user: %{id: user_id}, token: token} do
body = %{"content" => "new node content", "token" => token}
test "creates a node if content is present", %{
conn: conn,
user: %{id: user_id},
show_id: show_id,
token: token
} do
body = %{"content" => "new node content", "token" => token, show_id: show_id}
conn = post(conn, ~p"/api/v1/outline", body)

%{"uuid" => uuid} = json_response(conn, 200)

assert %{content: "new node content", creator_id: ^user_id} = Outline.get_node!(uuid)
assert %{content: "new node content", creator_id: ^user_id} =
Outline.get_node!(uuid)
end

test "created node is connected to episode", %{
conn: conn,
show_id: show_id,
token: token
} do
body = %{"content" => "new node content", "token" => token, show_id: show_id}
conn = post(conn, ~p"/api/v1/outline", body)

%{"uuid" => uuid} = json_response(conn, 200)

episode_id = Podcast.get_current_episode_for_show(show_id).id
assert %{content: "new node content", episode_id: ^episode_id} = Outline.get_node!(uuid)
end

test "can't create node when content is missing", %{conn: conn} do
Expand All @@ -33,8 +54,11 @@ defmodule RadiatorWeb.Api.OutlineControllerTest do
assert %{"error" => "missing params"} = json_response(conn, 400)
end

test "can't create node when token is wrong", %{conn: conn} do
body = %{"content" => "new node content", "token" => "invalid"}
test "can't create node when token is wrong", %{
conn: conn,
show_id: show_id
} do
body = %{"content" => "new node content", "token" => "invalid", show_id: show_id}
conn = post(conn, ~p"/api/v1/outline", body)

assert %{"error" => "params"} = json_response(conn, 400)
Expand Down
8 changes: 7 additions & 1 deletion test/support/fixtures/outline_fixtures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ defmodule Radiator.OutlineFixtures do
This module defines test helpers for creating
entities via the `Radiator.Outline` context.
"""
alias Radiator.PodcastFixtures

@doc """
Generate a node.
"""
def node_fixture(attrs \\ %{}) do
episode = PodcastFixtures.episode_fixture()

{:ok, node} =
attrs
|> Enum.into(%{content: "some content"})
|> Enum.into(%{
content: "some content",
episode_id: episode.id
})
|> Radiator.Outline.create_node()

node
Expand Down

0 comments on commit 787c2b1

Please sign in to comment.