diff --git a/lib/outlook/articles.ex b/lib/outlook/articles.ex
new file mode 100644
index 0000000..31c9681
--- /dev/null
+++ b/lib/outlook/articles.ex
@@ -0,0 +1,104 @@
+defmodule Outlook.Articles do
+ @moduledoc """
+ The Articles context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Outlook.Repo
+
+ alias Outlook.Articles.Article
+
+ @doc """
+ Returns the list of articles.
+
+ ## Examples
+
+ iex> list_articles()
+ [%Article{}, ...]
+
+ """
+ def list_articles do
+ Repo.all(Article)
+ end
+
+ @doc """
+ Gets a single article.
+
+ Raises `Ecto.NoResultsError` if the Article does not exist.
+
+ ## Examples
+
+ iex> get_article!(123)
+ %Article{}
+
+ iex> get_article!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_article!(id), do: Repo.get!(Article, id)
+
+ @doc """
+ Creates a article.
+
+ ## Examples
+
+ iex> create_article(%{field: value})
+ {:ok, %Article{}}
+
+ iex> create_article(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_article(attrs \\ %{}) do
+ %Article{}
+ |> Article.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a article.
+
+ ## Examples
+
+ iex> update_article(article, %{field: new_value})
+ {:ok, %Article{}}
+
+ iex> update_article(article, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_article(%Article{} = article, attrs) do
+ article
+ |> Article.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a article.
+
+ ## Examples
+
+ iex> delete_article(article)
+ {:ok, %Article{}}
+
+ iex> delete_article(article)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_article(%Article{} = article) do
+ Repo.delete(article)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking article changes.
+
+ ## Examples
+
+ iex> change_article(article)
+ %Ecto.Changeset{data: %Article{}}
+
+ """
+ def change_article(%Article{} = article, attrs \\ %{}) do
+ Article.changeset(article, attrs)
+ end
+end
diff --git a/lib/outlook/articles/article.ex b/lib/outlook/articles/article.ex
new file mode 100644
index 0000000..16afac8
--- /dev/null
+++ b/lib/outlook/articles/article.ex
@@ -0,0 +1,24 @@
+defmodule Outlook.Articles.Article do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ alias Outlook.Authors.Author
+
+ schema "articles" do
+ field :content, :string
+ field :date, :utc_datetime
+ field :language, :string
+ field :title, :string
+ field :url, :string
+ belongs_to :author, Author
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(article, attrs) do
+ article
+ |> cast(attrs, [:title, :content, :url, :language, :date])
+ |> validate_required([:title, :content, :url, :language, :date])
+ end
+end
diff --git a/lib/outlook/authors/author.ex b/lib/outlook/authors/author.ex
index 38b12c6..626f8d8 100644
--- a/lib/outlook/authors/author.ex
+++ b/lib/outlook/authors/author.ex
@@ -2,11 +2,14 @@ defmodule Outlook.Authors.Author do
use Ecto.Schema
import Ecto.Changeset
+ alias Outlook.Articles.Article
+
schema "authors" do
field :description, :string
field :homepage_name, :string
field :homepage_url, :string
field :name, :string
+ has_many :articles, Article
timestamps()
end
diff --git a/lib/outlook_web/live/article_live/form_component.ex b/lib/outlook_web/live/article_live/form_component.ex
new file mode 100644
index 0000000..6801087
--- /dev/null
+++ b/lib/outlook_web/live/article_live/form_component.ex
@@ -0,0 +1,85 @@
+defmodule OutlookWeb.ArticleLive.FormComponent do
+ use OutlookWeb, :live_component
+
+ alias Outlook.Articles
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ <%= @title %>
+ <:subtitle>Use this form to manage article records in your database.
+
+
+ <.simple_form
+ :let={f}
+ for={@changeset}
+ id="article-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input field={{f, :title}} type="text" label="title" />
+ <.input field={{f, :content}} type="text" label="content" />
+ <.input field={{f, :url}} type="text" label="url" />
+ <.input field={{f, :language}} type="text" label="language" />
+ <.input field={{f, :date}} type="datetime-local" label="date" />
+ <:actions>
+ <.button phx-disable-with="Saving...">Save Article
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{article: article} = assigns, socket) do
+ changeset = Articles.change_article(article)
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:changeset, changeset)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"article" => article_params}, socket) do
+ changeset =
+ socket.assigns.article
+ |> Articles.change_article(article_params)
+ |> Map.put(:action, :validate)
+
+ {:noreply, assign(socket, :changeset, changeset)}
+ end
+
+ def handle_event("save", %{"article" => article_params}, socket) do
+ save_article(socket, socket.assigns.action, article_params)
+ end
+
+ defp save_article(socket, :edit, article_params) do
+ case Articles.update_article(socket.assigns.article, article_params) do
+ {:ok, _article} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Article updated successfully")
+ |> push_navigate(to: socket.assigns.navigate)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, :changeset, changeset)}
+ end
+ end
+
+ defp save_article(socket, :new, article_params) do
+ case Articles.create_article(article_params) do
+ {:ok, _article} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Article created successfully")
+ |> push_navigate(to: socket.assigns.navigate)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, changeset: changeset)}
+ end
+ end
+end
diff --git a/lib/outlook_web/live/article_live/index.ex b/lib/outlook_web/live/article_live/index.ex
new file mode 100644
index 0000000..1174d5d
--- /dev/null
+++ b/lib/outlook_web/live/article_live/index.ex
@@ -0,0 +1,46 @@
+defmodule OutlookWeb.ArticleLive.Index do
+ use OutlookWeb, :live_view
+
+ alias Outlook.Articles
+ alias Outlook.Articles.Article
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, assign(socket, :articles, list_articles())}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Article")
+ |> assign(:article, Articles.get_article!(id))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Article")
+ |> assign(:article, %Article{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Articles")
+ |> assign(:article, nil)
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ article = Articles.get_article!(id)
+ {:ok, _} = Articles.delete_article(article)
+
+ {:noreply, assign(socket, :articles, list_articles())}
+ end
+
+ defp list_articles do
+ Articles.list_articles()
+ end
+end
diff --git a/lib/outlook_web/live/article_live/index.html.heex b/lib/outlook_web/live/article_live/index.html.heex
new file mode 100644
index 0000000..61a6418
--- /dev/null
+++ b/lib/outlook_web/live/article_live/index.html.heex
@@ -0,0 +1,43 @@
+<.header>
+ Listing Articles
+ <:actions>
+ <.link patch={~p"/articles/new"}>
+ <.button>New Article
+
+
+
+
+<.table id="articles" rows={@articles} row_click={&JS.navigate(~p"/articles/#{&1}")}>
+ <:col :let={article} label="Title"><%= article.title %>
+ <:col :let={article} label="Content"><%= article.content %>
+ <:col :let={article} label="Url"><%= article.url %>
+ <:col :let={article} label="Language"><%= article.language %>
+ <:col :let={article} label="Date"><%= article.date %>
+ <:action :let={article}>
+
+ <.link navigate={~p"/articles/#{article}"}>Show
+
+ <.link patch={~p"/articles/#{article}/edit"}>Edit
+
+ <:action :let={article}>
+ <.link phx-click={JS.push("delete", value: %{id: article.id})} data-confirm="Are you sure?">
+ Delete
+
+
+
+
+<.modal
+ :if={@live_action in [:new, :edit]}
+ id="article-modal"
+ show
+ on_cancel={JS.navigate(~p"/articles")}
+>
+ <.live_component
+ module={OutlookWeb.ArticleLive.FormComponent}
+ id={@article.id || :new}
+ title={@page_title}
+ action={@live_action}
+ article={@article}
+ navigate={~p"/articles"}
+ />
+
diff --git a/lib/outlook_web/live/article_live/show.ex b/lib/outlook_web/live/article_live/show.ex
new file mode 100644
index 0000000..cf86edc
--- /dev/null
+++ b/lib/outlook_web/live/article_live/show.ex
@@ -0,0 +1,21 @@
+defmodule OutlookWeb.ArticleLive.Show do
+ use OutlookWeb, :live_view
+
+ alias Outlook.Articles
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_params(%{"id" => id}, _, socket) do
+ {:noreply,
+ socket
+ |> assign(:page_title, page_title(socket.assigns.live_action))
+ |> assign(:article, Articles.get_article!(id))}
+ end
+
+ defp page_title(:show), do: "Show Article"
+ defp page_title(:edit), do: "Edit Article"
+end
diff --git a/lib/outlook_web/live/article_live/show.html.heex b/lib/outlook_web/live/article_live/show.html.heex
new file mode 100644
index 0000000..29321ec
--- /dev/null
+++ b/lib/outlook_web/live/article_live/show.html.heex
@@ -0,0 +1,30 @@
+<.header>
+ Article <%= @article.id %>
+ <:subtitle>This is a article record from your database.
+ <:actions>
+ <.link patch={~p"/articles/#{@article}/show/edit"} phx-click={JS.push_focus()}>
+ <.button>Edit article
+
+
+
+
+<.list>
+ <:item title="Title"><%= @article.title %>
+ <:item title="Content"><%= @article.content %>
+ <:item title="Url"><%= @article.url %>
+ <:item title="Language"><%= @article.language %>
+ <:item title="Date"><%= @article.date %>
+
+
+<.back navigate={~p"/articles"}>Back to articles
+
+<.modal :if={@live_action == :edit} id="article-modal" show on_cancel={JS.patch(~p"/articles/#{@article}")}>
+ <.live_component
+ module={OutlookWeb.ArticleLive.FormComponent}
+ id={@article.id}
+ title={@page_title}
+ action={@live_action}
+ article={@article}
+ navigate={~p"/articles/#{@article}"}
+ />
+
diff --git a/lib/outlook_web/router.ex b/lib/outlook_web/router.ex
index c2f3205..f6fa0b7 100644
--- a/lib/outlook_web/router.ex
+++ b/lib/outlook_web/router.ex
@@ -76,6 +76,13 @@ defmodule OutlookWeb.Router do
live "/authors/:id", AuthorLive.Show, :show
live "/authors/:id/show/edit", AuthorLive.Show, :edit
+
+ live "/articles", ArticleLive.Index, :index
+ live "/articles/new", ArticleLive.Index, :new
+ live "/articles/:id/edit", ArticleLive.Index, :edit
+
+ live "/articles/:id", ArticleLive.Show, :show
+ live "/articles/:id/show/edit", ArticleLive.Show, :edit
end
scope "/", OutlookWeb do
diff --git a/priv/repo/migrations/20221226161611_create_articles.exs b/priv/repo/migrations/20221226161611_create_articles.exs
new file mode 100644
index 0000000..37c4a26
--- /dev/null
+++ b/priv/repo/migrations/20221226161611_create_articles.exs
@@ -0,0 +1,18 @@
+defmodule Outlook.Repo.Migrations.CreateArticles do
+ use Ecto.Migration
+
+ def change do
+ create table(:articles) do
+ add :title, :string
+ add :content, :text
+ add :url, :string
+ add :language, :string
+ add :date, :utc_datetime
+ add :author_id, references(:authors, on_delete: :nothing)
+
+ timestamps()
+ end
+
+ create index(:articles, [:author_id])
+ end
+end
diff --git a/test/outlook/articles_test.exs b/test/outlook/articles_test.exs
new file mode 100644
index 0000000..f185923
--- /dev/null
+++ b/test/outlook/articles_test.exs
@@ -0,0 +1,67 @@
+defmodule Outlook.ArticlesTest do
+ use Outlook.DataCase
+
+ alias Outlook.Articles
+
+ describe "articles" do
+ alias Outlook.Articles.Article
+
+ import Outlook.ArticlesFixtures
+
+ @invalid_attrs %{content: nil, date: nil, language: nil, title: nil, url: nil}
+
+ test "list_articles/0 returns all articles" do
+ article = article_fixture()
+ assert Articles.list_articles() == [article]
+ end
+
+ test "get_article!/1 returns the article with given id" do
+ article = article_fixture()
+ assert Articles.get_article!(article.id) == article
+ end
+
+ test "create_article/1 with valid data creates a article" do
+ valid_attrs = %{content: "some content", date: ~U[2022-12-25 16:16:00Z], language: "some language", title: "some title", url: "some url"}
+
+ assert {:ok, %Article{} = article} = Articles.create_article(valid_attrs)
+ assert article.content == "some content"
+ assert article.date == ~U[2022-12-25 16:16:00Z]
+ assert article.language == "some language"
+ assert article.title == "some title"
+ assert article.url == "some url"
+ end
+
+ test "create_article/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Articles.create_article(@invalid_attrs)
+ end
+
+ test "update_article/2 with valid data updates the article" do
+ article = article_fixture()
+ update_attrs = %{content: "some updated content", date: ~U[2022-12-26 16:16:00Z], language: "some updated language", title: "some updated title", url: "some updated url"}
+
+ assert {:ok, %Article{} = article} = Articles.update_article(article, update_attrs)
+ assert article.content == "some updated content"
+ assert article.date == ~U[2022-12-26 16:16:00Z]
+ assert article.language == "some updated language"
+ assert article.title == "some updated title"
+ assert article.url == "some updated url"
+ end
+
+ test "update_article/2 with invalid data returns error changeset" do
+ article = article_fixture()
+ assert {:error, %Ecto.Changeset{}} = Articles.update_article(article, @invalid_attrs)
+ assert article == Articles.get_article!(article.id)
+ end
+
+ test "delete_article/1 deletes the article" do
+ article = article_fixture()
+ assert {:ok, %Article{}} = Articles.delete_article(article)
+ assert_raise Ecto.NoResultsError, fn -> Articles.get_article!(article.id) end
+ end
+
+ test "change_article/1 returns a article changeset" do
+ article = article_fixture()
+ assert %Ecto.Changeset{} = Articles.change_article(article)
+ end
+ end
+end
diff --git a/test/outlook_web/live/article_live_test.exs b/test/outlook_web/live/article_live_test.exs
new file mode 100644
index 0000000..0b0b050
--- /dev/null
+++ b/test/outlook_web/live/article_live_test.exs
@@ -0,0 +1,110 @@
+defmodule OutlookWeb.ArticleLiveTest do
+ use OutlookWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import Outlook.ArticlesFixtures
+
+ @create_attrs %{content: "some content", date: "2022-12-25T16:16:00Z", language: "some language", title: "some title", url: "some url"}
+ @update_attrs %{content: "some updated content", date: "2022-12-26T16:16:00Z", language: "some updated language", title: "some updated title", url: "some updated url"}
+ @invalid_attrs %{content: nil, date: nil, language: nil, title: nil, url: nil}
+
+ defp create_article(_) do
+ article = article_fixture()
+ %{article: article}
+ end
+
+ describe "Index" do
+ setup [:create_article]
+
+ test "lists all articles", %{conn: conn, article: article} do
+ {:ok, _index_live, html} = live(conn, ~p"/articles")
+
+ assert html =~ "Listing Articles"
+ assert html =~ article.content
+ end
+
+ test "saves new article", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, ~p"/articles")
+
+ assert index_live |> element("a", "New Article") |> render_click() =~
+ "New Article"
+
+ assert_patch(index_live, ~p"/articles/new")
+
+ assert index_live
+ |> form("#article-form", article: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ {:ok, _, html} =
+ index_live
+ |> form("#article-form", article: @create_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/articles")
+
+ assert html =~ "Article created successfully"
+ assert html =~ "some content"
+ end
+
+ test "updates article in listing", %{conn: conn, article: article} do
+ {:ok, index_live, _html} = live(conn, ~p"/articles")
+
+ assert index_live |> element("#articles-#{article.id} a", "Edit") |> render_click() =~
+ "Edit Article"
+
+ assert_patch(index_live, ~p"/articles/#{article}/edit")
+
+ assert index_live
+ |> form("#article-form", article: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ {:ok, _, html} =
+ index_live
+ |> form("#article-form", article: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/articles")
+
+ assert html =~ "Article updated successfully"
+ assert html =~ "some updated content"
+ end
+
+ test "deletes article in listing", %{conn: conn, article: article} do
+ {:ok, index_live, _html} = live(conn, ~p"/articles")
+
+ assert index_live |> element("#articles-#{article.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#article-#{article.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_article]
+
+ test "displays article", %{conn: conn, article: article} do
+ {:ok, _show_live, html} = live(conn, ~p"/articles/#{article}")
+
+ assert html =~ "Show Article"
+ assert html =~ article.content
+ end
+
+ test "updates article within modal", %{conn: conn, article: article} do
+ {:ok, show_live, _html} = live(conn, ~p"/articles/#{article}")
+
+ assert show_live |> element("a", "Edit") |> render_click() =~
+ "Edit Article"
+
+ assert_patch(show_live, ~p"/articles/#{article}/show/edit")
+
+ assert show_live
+ |> form("#article-form", article: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ {:ok, _, html} =
+ show_live
+ |> form("#article-form", article: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/articles/#{article}")
+
+ assert html =~ "Article updated successfully"
+ assert html =~ "some updated content"
+ end
+ end
+end
diff --git a/test/support/fixtures/articles_fixtures.ex b/test/support/fixtures/articles_fixtures.ex
new file mode 100644
index 0000000..bab4783
--- /dev/null
+++ b/test/support/fixtures/articles_fixtures.ex
@@ -0,0 +1,24 @@
+defmodule Outlook.ArticlesFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Outlook.Articles` context.
+ """
+
+ @doc """
+ Generate a article.
+ """
+ def article_fixture(attrs \\ %{}) do
+ {:ok, article} =
+ attrs
+ |> Enum.into(%{
+ content: "some content",
+ date: ~U[2022-12-25 16:16:00Z],
+ language: "some language",
+ title: "some title",
+ url: "some url"
+ })
+ |> Outlook.Articles.create_article()
+
+ article
+ end
+end