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

Connect outline nodes to episodes #510

Merged
merged 7 commits into from
Jan 14, 2024
Merged
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
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