diff --git a/lib/outlook/articles/article.ex b/lib/outlook/articles/article.ex index 16afac8..9c4eabd 100644 --- a/lib/outlook/articles/article.ex +++ b/lib/outlook/articles/article.ex @@ -11,6 +11,7 @@ defmodule Outlook.Articles.Article do field :title, :string field :url, :string belongs_to :author, Author + has_many :translations, Translation timestamps() end diff --git a/lib/outlook/translations.ex b/lib/outlook/translations.ex new file mode 100644 index 0000000..b94a777 --- /dev/null +++ b/lib/outlook/translations.ex @@ -0,0 +1,104 @@ +defmodule Outlook.Translations do + @moduledoc """ + The Translations context. + """ + + import Ecto.Query, warn: false + alias Outlook.Repo + + alias Outlook.Translations.Translation + + @doc """ + Returns the list of translations. + + ## Examples + + iex> list_translations() + [%Translation{}, ...] + + """ + def list_translations do + Repo.all(Translation) + end + + @doc """ + Gets a single translation. + + Raises `Ecto.NoResultsError` if the Translation does not exist. + + ## Examples + + iex> get_translation!(123) + %Translation{} + + iex> get_translation!(456) + ** (Ecto.NoResultsError) + + """ + def get_translation!(id), do: Repo.get!(Translation, id) + + @doc """ + Creates a translation. + + ## Examples + + iex> create_translation(%{field: value}) + {:ok, %Translation{}} + + iex> create_translation(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_translation(attrs \\ %{}) do + %Translation{} + |> Translation.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a translation. + + ## Examples + + iex> update_translation(translation, %{field: new_value}) + {:ok, %Translation{}} + + iex> update_translation(translation, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_translation(%Translation{} = translation, attrs) do + translation + |> Translation.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a translation. + + ## Examples + + iex> delete_translation(translation) + {:ok, %Translation{}} + + iex> delete_translation(translation) + {:error, %Ecto.Changeset{}} + + """ + def delete_translation(%Translation{} = translation) do + Repo.delete(translation) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking translation changes. + + ## Examples + + iex> change_translation(translation) + %Ecto.Changeset{data: %Translation{}} + + """ + def change_translation(%Translation{} = translation, attrs \\ %{}) do + Translation.changeset(translation, attrs) + end +end diff --git a/lib/outlook/translations/translation.ex b/lib/outlook/translations/translation.ex new file mode 100644 index 0000000..8f1c3fb --- /dev/null +++ b/lib/outlook/translations/translation.ex @@ -0,0 +1,28 @@ +defmodule Outlook.Translations.Translation do + use Ecto.Schema + import Ecto.Changeset + + alias Outlook.Accounts.User + alias Outlook.Articles.Article + + schema "translations" do + field :content, :map + field :date, :utc_datetime + field :lang, :string + field :public, :boolean, default: false + field :teaser, :string + field :title, :string + field :unauthorized, :boolean, default: false + belongs_to :user, User + belongs_to :article, Article + + timestamps() + end + + @doc false + def changeset(translation, attrs) do + translation + |> cast(attrs, [:lang, :title, :teaser, :content, :date, :public, :unauthorized]) + |> validate_required([:lang, :title, :teaser, :content, :date, :public, :unauthorized]) + end +end diff --git a/lib/outlook_web/live/translation_live/form_component.ex b/lib/outlook_web/live/translation_live/form_component.ex new file mode 100644 index 0000000..a08bbe7 --- /dev/null +++ b/lib/outlook_web/live/translation_live/form_component.ex @@ -0,0 +1,87 @@ +defmodule OutlookWeb.TranslationLive.FormComponent do + use OutlookWeb, :live_component + + alias Outlook.Translations + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage translation records in your database. + + + <.simple_form + :let={f} + for={@changeset} + id="translation-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={{f, :lang}} type="text" label="lang" /> + <.input field={{f, :title}} type="text" label="title" /> + <.input field={{f, :teaser}} type="text" label="teaser" /> + <.input field={{f, :content}} type="text" label="content" /> + <.input field={{f, :date}} type="datetime-local" label="date" /> + <.input field={{f, :public}} type="checkbox" label="public" /> + <.input field={{f, :unauthorized}} type="checkbox" label="unauthorized" /> + <:actions> + <.button phx-disable-with="Saving...">Save Translation + + +
+ """ + end + + @impl true + def update(%{translation: translation} = assigns, socket) do + changeset = Translations.change_translation(translation) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"translation" => translation_params}, socket) do + changeset = + socket.assigns.translation + |> Translations.change_translation(translation_params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"translation" => translation_params}, socket) do + save_translation(socket, socket.assigns.action, translation_params) + end + + defp save_translation(socket, :edit, translation_params) do + case Translations.update_translation(socket.assigns.translation, translation_params) do + {:ok, _translation} -> + {:noreply, + socket + |> put_flash(:info, "Translation updated successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_translation(socket, :new, translation_params) do + case Translations.create_translation(translation_params) do + {:ok, _translation} -> + {:noreply, + socket + |> put_flash(:info, "Translation 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/translation_live/index.ex b/lib/outlook_web/live/translation_live/index.ex new file mode 100644 index 0000000..8a76e2d --- /dev/null +++ b/lib/outlook_web/live/translation_live/index.ex @@ -0,0 +1,46 @@ +defmodule OutlookWeb.TranslationLive.Index do + use OutlookWeb, :live_view + + alias Outlook.Translations + alias Outlook.Translations.Translation + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :translations, list_translations())} + 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 Translation") + |> assign(:translation, Translations.get_translation!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Translation") + |> assign(:translation, %Translation{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Translations") + |> assign(:translation, nil) + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + translation = Translations.get_translation!(id) + {:ok, _} = Translations.delete_translation(translation) + + {:noreply, assign(socket, :translations, list_translations())} + end + + defp list_translations do + Translations.list_translations() + end +end diff --git a/lib/outlook_web/live/translation_live/index.html.heex b/lib/outlook_web/live/translation_live/index.html.heex new file mode 100644 index 0000000..e0379a1 --- /dev/null +++ b/lib/outlook_web/live/translation_live/index.html.heex @@ -0,0 +1,45 @@ +<.header> + Listing Translations + <:actions> + <.link patch={~p"/translations/new"}> + <.button>New Translation + + + + +<.table id="translations" rows={@translations} row_click={&JS.navigate(~p"/translations/#{&1}")}> + <:col :let={translation} label="Lang"><%= translation.lang %> + <:col :let={translation} label="Title"><%= translation.title %> + <:col :let={translation} label="Teaser"><%= translation.teaser %> + <:col :let={translation} label="Content"><%= translation.content %> + <:col :let={translation} label="Date"><%= translation.date %> + <:col :let={translation} label="Public"><%= translation.public %> + <:col :let={translation} label="Unauthorized"><%= translation.unauthorized %> + <:action :let={translation}> +
+ <.link navigate={~p"/translations/#{translation}"}>Show +
+ <.link patch={~p"/translations/#{translation}/edit"}>Edit + + <:action :let={translation}> + <.link phx-click={JS.push("delete", value: %{id: translation.id})} data-confirm="Are you sure?"> + Delete + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="translation-modal" + show + on_cancel={JS.navigate(~p"/translations")} +> + <.live_component + module={OutlookWeb.TranslationLive.FormComponent} + id={@translation.id || :new} + title={@page_title} + action={@live_action} + translation={@translation} + navigate={~p"/translations"} + /> + diff --git a/lib/outlook_web/live/translation_live/show.ex b/lib/outlook_web/live/translation_live/show.ex new file mode 100644 index 0000000..6564e54 --- /dev/null +++ b/lib/outlook_web/live/translation_live/show.ex @@ -0,0 +1,21 @@ +defmodule OutlookWeb.TranslationLive.Show do + use OutlookWeb, :live_view + + alias Outlook.Translations + + @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(:translation, Translations.get_translation!(id))} + end + + defp page_title(:show), do: "Show Translation" + defp page_title(:edit), do: "Edit Translation" +end diff --git a/lib/outlook_web/live/translation_live/show.html.heex b/lib/outlook_web/live/translation_live/show.html.heex new file mode 100644 index 0000000..8703f90 --- /dev/null +++ b/lib/outlook_web/live/translation_live/show.html.heex @@ -0,0 +1,32 @@ +<.header> + Translation <%= @translation.id %> + <:subtitle>This is a translation record from your database. + <:actions> + <.link patch={~p"/translations/#{@translation}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit translation + + + + +<.list> + <:item title="Lang"><%= @translation.lang %> + <:item title="Title"><%= @translation.title %> + <:item title="Teaser"><%= @translation.teaser %> + <:item title="Content"><%= @translation.content %> + <:item title="Date"><%= @translation.date %> + <:item title="Public"><%= @translation.public %> + <:item title="Unauthorized"><%= @translation.unauthorized %> + + +<.back navigate={~p"/translations"}>Back to translations + +<.modal :if={@live_action == :edit} id="translation-modal" show on_cancel={JS.patch(~p"/translations/#{@translation}")}> + <.live_component + module={OutlookWeb.TranslationLive.FormComponent} + id={@translation.id} + title={@page_title} + action={@live_action} + translation={@translation} + navigate={~p"/translations/#{@translation}"} + /> + diff --git a/lib/outlook_web/router.ex b/lib/outlook_web/router.ex index f6fa0b7..c189ead 100644 --- a/lib/outlook_web/router.ex +++ b/lib/outlook_web/router.ex @@ -83,6 +83,13 @@ defmodule OutlookWeb.Router do live "/articles/:id", ArticleLive.Show, :show live "/articles/:id/show/edit", ArticleLive.Show, :edit + + live "/translations", TranslationLive.Index, :index + live "/translations/new", TranslationLive.Index, :new + live "/translations/:id/edit", TranslationLive.Index, :edit + + live "/translations/:id", TranslationLive.Show, :show + live "/translations/:id/show/edit", TranslationLive.Show, :edit end scope "/", OutlookWeb do diff --git a/priv/repo/migrations/20221226171313_create_translations.exs b/priv/repo/migrations/20221226171313_create_translations.exs new file mode 100644 index 0000000..b65a747 --- /dev/null +++ b/priv/repo/migrations/20221226171313_create_translations.exs @@ -0,0 +1,22 @@ +defmodule Outlook.Repo.Migrations.CreateTranslations do + use Ecto.Migration + + def change do + create table(:translations) do + add :lang, :string + add :title, :string + add :teaser, :text + add :content, :map + add :date, :utc_datetime + add :public, :boolean, default: false, null: false + add :unauthorized, :boolean, default: false, null: false + add :user_id, references(:users, on_delete: :nothing) + add :article_id, references(:articles, on_delete: :nothing) + + timestamps() + end + + create index(:translations, [:user_id]) + create index(:translations, [:article_id]) + end +end diff --git a/test/outlook/translations_test.exs b/test/outlook/translations_test.exs new file mode 100644 index 0000000..acf52e0 --- /dev/null +++ b/test/outlook/translations_test.exs @@ -0,0 +1,71 @@ +defmodule Outlook.TranslationsTest do + use Outlook.DataCase + + alias Outlook.Translations + + describe "translations" do + alias Outlook.Translations.Translation + + import Outlook.TranslationsFixtures + + @invalid_attrs %{content: nil, date: nil, lang: nil, public: nil, teaser: nil, title: nil, unauthorized: nil} + + test "list_translations/0 returns all translations" do + translation = translation_fixture() + assert Translations.list_translations() == [translation] + end + + test "get_translation!/1 returns the translation with given id" do + translation = translation_fixture() + assert Translations.get_translation!(translation.id) == translation + end + + test "create_translation/1 with valid data creates a translation" do + valid_attrs = %{content: %{}, date: ~U[2022-12-25 17:13:00Z], lang: "some lang", public: true, teaser: "some teaser", title: "some title", unauthorized: true} + + assert {:ok, %Translation{} = translation} = Translations.create_translation(valid_attrs) + assert translation.content == %{} + assert translation.date == ~U[2022-12-25 17:13:00Z] + assert translation.lang == "some lang" + assert translation.public == true + assert translation.teaser == "some teaser" + assert translation.title == "some title" + assert translation.unauthorized == true + end + + test "create_translation/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Translations.create_translation(@invalid_attrs) + end + + test "update_translation/2 with valid data updates the translation" do + translation = translation_fixture() + update_attrs = %{content: %{}, date: ~U[2022-12-26 17:13:00Z], lang: "some updated lang", public: false, teaser: "some updated teaser", title: "some updated title", unauthorized: false} + + assert {:ok, %Translation{} = translation} = Translations.update_translation(translation, update_attrs) + assert translation.content == %{} + assert translation.date == ~U[2022-12-26 17:13:00Z] + assert translation.lang == "some updated lang" + assert translation.public == false + assert translation.teaser == "some updated teaser" + assert translation.title == "some updated title" + assert translation.unauthorized == false + end + + test "update_translation/2 with invalid data returns error changeset" do + translation = translation_fixture() + assert {:error, %Ecto.Changeset{}} = Translations.update_translation(translation, @invalid_attrs) + assert translation == Translations.get_translation!(translation.id) + end + + test "delete_translation/1 deletes the translation" do + translation = translation_fixture() + assert {:ok, %Translation{}} = Translations.delete_translation(translation) + assert_raise Ecto.NoResultsError, fn -> Translations.get_translation!(translation.id) end + end + + test "change_translation/1 returns a translation changeset" do + translation = translation_fixture() + assert %Ecto.Changeset{} = Translations.change_translation(translation) + end + end +end diff --git a/test/outlook_web/live/translation_live_test.exs b/test/outlook_web/live/translation_live_test.exs new file mode 100644 index 0000000..9d48aaf --- /dev/null +++ b/test/outlook_web/live/translation_live_test.exs @@ -0,0 +1,110 @@ +defmodule OutlookWeb.TranslationLiveTest do + use OutlookWeb.ConnCase + + import Phoenix.LiveViewTest + import Outlook.TranslationsFixtures + + @create_attrs %{content: %{}, date: "2022-12-25T17:13:00Z", lang: "some lang", public: true, teaser: "some teaser", title: "some title", unauthorized: true} + @update_attrs %{content: %{}, date: "2022-12-26T17:13:00Z", lang: "some updated lang", public: false, teaser: "some updated teaser", title: "some updated title", unauthorized: false} + @invalid_attrs %{content: nil, date: nil, lang: nil, public: false, teaser: nil, title: nil, unauthorized: false} + + defp create_translation(_) do + translation = translation_fixture() + %{translation: translation} + end + + describe "Index" do + setup [:create_translation] + + test "lists all translations", %{conn: conn, translation: translation} do + {:ok, _index_live, html} = live(conn, ~p"/translations") + + assert html =~ "Listing Translations" + assert html =~ translation.lang + end + + test "saves new translation", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/translations") + + assert index_live |> element("a", "New Translation") |> render_click() =~ + "New Translation" + + assert_patch(index_live, ~p"/translations/new") + + assert index_live + |> form("#translation-form", translation: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#translation-form", translation: @create_attrs) + |> render_submit() + |> follow_redirect(conn, ~p"/translations") + + assert html =~ "Translation created successfully" + assert html =~ "some lang" + end + + test "updates translation in listing", %{conn: conn, translation: translation} do + {:ok, index_live, _html} = live(conn, ~p"/translations") + + assert index_live |> element("#translations-#{translation.id} a", "Edit") |> render_click() =~ + "Edit Translation" + + assert_patch(index_live, ~p"/translations/#{translation}/edit") + + assert index_live + |> form("#translation-form", translation: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#translation-form", translation: @update_attrs) + |> render_submit() + |> follow_redirect(conn, ~p"/translations") + + assert html =~ "Translation updated successfully" + assert html =~ "some updated lang" + end + + test "deletes translation in listing", %{conn: conn, translation: translation} do + {:ok, index_live, _html} = live(conn, ~p"/translations") + + assert index_live |> element("#translations-#{translation.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#translation-#{translation.id}") + end + end + + describe "Show" do + setup [:create_translation] + + test "displays translation", %{conn: conn, translation: translation} do + {:ok, _show_live, html} = live(conn, ~p"/translations/#{translation}") + + assert html =~ "Show Translation" + assert html =~ translation.lang + end + + test "updates translation within modal", %{conn: conn, translation: translation} do + {:ok, show_live, _html} = live(conn, ~p"/translations/#{translation}") + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Translation" + + assert_patch(show_live, ~p"/translations/#{translation}/show/edit") + + assert show_live + |> form("#translation-form", translation: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + show_live + |> form("#translation-form", translation: @update_attrs) + |> render_submit() + |> follow_redirect(conn, ~p"/translations/#{translation}") + + assert html =~ "Translation updated successfully" + assert html =~ "some updated lang" + end + end +end diff --git a/test/support/fixtures/translations_fixtures.ex b/test/support/fixtures/translations_fixtures.ex new file mode 100644 index 0000000..16f4d8d --- /dev/null +++ b/test/support/fixtures/translations_fixtures.ex @@ -0,0 +1,26 @@ +defmodule Outlook.TranslationsFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Outlook.Translations` context. + """ + + @doc """ + Generate a translation. + """ + def translation_fixture(attrs \\ %{}) do + {:ok, translation} = + attrs + |> Enum.into(%{ + content: %{}, + date: ~U[2022-12-25 17:13:00Z], + lang: "some lang", + public: true, + teaser: "some teaser", + title: "some title", + unauthorized: true + }) + |> Outlook.Translations.create_translation() + + translation + end +end