diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..61a7393 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,45 @@ +# This file excludes paths from the Docker build context. +# +# By default, Docker's build context includes all files (and folders) in the +# current directory. Even if a file isn't copied into the container it is still sent to +# the Docker daemon. +# +# There are multiple reasons to exclude files from the build context: +# +# 1. Prevent nested folders from being copied into the container (ex: exclude +# /assets/node_modules when copying /assets) +# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) +# 3. Avoid sending files containing sensitive information +# +# More information on using .dockerignore is available here: +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +.dockerignore + +# Ignore git, but keep git HEAD and refs to access current commit hash if needed: +# +# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat +# d0b8727759e1e0e7aa3d41707d12376e373d5ecc +.git +!.git/HEAD +!.git/refs + +# Common development/test artifacts +/cover/ +/doc/ +/test/ +/tmp/ +.elixir_ls + +# Mix artifacts +/_build/ +/deps/ +*.ez + +# Generated on crash by the VM +erl_crash.dump + +# Static artifacts - These should be fetched and built inside the Docker image +/assets/node_modules/ +/priv/static/assets/ +/priv/static/cache_manifest.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d301bbd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,97 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian +# instead of Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu +# https://hub.docker.com/_/ubuntu?tab=tags +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20240513-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: hexpm/elixir:1.15.7-erlang-26.1.2-debian-bullseye-20240513-slim +# +ARG ELIXIR_VERSION=1.15.7 +ARG OTP_VERSION=26.1.2 +ARG DEBIAN_VERSION=bullseye-20240513-slim + +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} as builder + +# install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +COPY lib lib + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && \ + apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# set runner ENV +ENV MIX_ENV="prod" + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/phoenixRealWorld ./ + +USER nobody + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +# ENTRYPOINT ["/tini", "--"] + +CMD ["/app/bin/server"] diff --git a/lib/phoenixRealWorld/release.ex b/lib/phoenixRealWorld/release.ex new file mode 100644 index 0000000..cea0eb4 --- /dev/null +++ b/lib/phoenixRealWorld/release.ex @@ -0,0 +1,28 @@ +defmodule PhoenixRealWorld.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :phoenixRealWorld + + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/lib/phoenixRealWorld_web/components/article_components.ex b/lib/phoenixRealWorld_web/components/article_components.ex new file mode 100644 index 0000000..08e2509 --- /dev/null +++ b/lib/phoenixRealWorld_web/components/article_components.ex @@ -0,0 +1,19 @@ +defmodule PhoenixRealWorldWeb.ArticleComponents do + use Phoenix.Component + + alias PhoenixRealWorld.Blogs.Article + + attr :article, Article, required: true + + def tags(assigns) do + tag_names = + assigns.article.tags + |> Enum.map(fn %{tag: tag} -> tag end) + |> Enum.join(", ") + assigns = assign(assigns, :tag_names, tag_names) + + ~H""" + <%= @tag_names %> + """ + end +end diff --git a/lib/phoenixRealWorld_web/live/article_live/form_component.ex b/lib/phoenixRealWorld_web/live/article_live/form_component.ex index 78498f2..e972404 100644 --- a/lib/phoenixRealWorld_web/live/article_live/form_component.ex +++ b/lib/phoenixRealWorld_web/live/article_live/form_component.ex @@ -51,8 +51,8 @@ defmodule PhoenixRealWorldWeb.ArticleLive.FormComponent do end defp save_article(socket, :edit, article_params) do - case Blogs.update_article(socket.assigns.article, article_params) do - {:ok, article} -> + case Blogs.insert_or_update_article_with_tags(socket.assigns.article, article_params) do #←変更 + {:ok, %{article: article}} -> #←変更 notify_parent({:saved, article}) {:noreply, @@ -66,9 +66,9 @@ defmodule PhoenixRealWorldWeb.ArticleLive.FormComponent do end defp save_article(socket, :new, article_params) do - article_params = Map.put(article_params, "user_id", socket.assigns.current_user.id) - case Blogs.create_article(article_params) do - {:ok, article} -> + article_params = Map.put(article_params, "author_id", socket.assigns.current_user.id) + case Blogs.insert_article_with_tags(article_params) do #←変更 + {:ok, %{article: article}} -> #←変更 notify_parent({:saved, article}) {:noreply, diff --git a/lib/phoenixRealWorld_web/live/article_live/index.ex b/lib/phoenixRealWorld_web/live/article_live/index.ex index 51e7ea9..30c05fc 100644 --- a/lib/phoenixRealWorld_web/live/article_live/index.ex +++ b/lib/phoenixRealWorld_web/live/article_live/index.ex @@ -4,9 +4,30 @@ defmodule PhoenixRealWorldWeb.ArticleLive.Index do alias PhoenixRealWorld.Blogs alias PhoenixRealWorld.Blogs.Article + # ↓削除 + # @impl true + # def mount(_params, _session, socket) do + # {:ok, stream(socket, :articles, Blogs.list_articles())} + # end + + # ↓追加 + @impl true + def mount(%{"tag" => tag}, _session, socket) do + {:ok, + socket + |> stream(:articles, Blogs.list_articles_by_tag(tag)) + |> assign(:tags, Blogs.list_tags()) + } + end + + # ↓追加 @impl true def mount(_params, _session, socket) do - {:ok, stream(socket, :articles, Blogs.list_articles())} + {:ok, + socket + |> stream(:articles, Blogs.list_articles()) + |> assign(:tags, Blogs.list_tags()) + } end @impl true diff --git a/lib/phoenixRealWorld_web/live/article_live/index.html.heex b/lib/phoenixRealWorld_web/live/article_live/index.html.heex index 16ca11d..f64f974 100644 --- a/lib/phoenixRealWorld_web/live/article_live/index.html.heex +++ b/lib/phoenixRealWorld_web/live/article_live/index.html.heex @@ -7,6 +7,20 @@ +<%!-- 追加 --%> +
+ Search by tag: + <.link + :for={%{tag: tag} <- @tags} + href={~p"/articles?tag=#{tag}"} + class="underline" + > + + <%= tag %> + + <.link href={~p"/articles"} class="text-yellow-700">Reset +
+ <.table id="articles" rows={@streams.articles} @@ -14,6 +28,11 @@ > <:col :let={{_id, article}} label="Title">{article.title} <:col :let={{_id, article}} label="Body">{article.body} + <%!-- ↓追加 --%> + <:col :let={{_id, article}} label="tags"> + + + <:action :let={{_id, article}}>
<.link navigate={~p"/articles/#{article}"}>Show diff --git a/lib/phoenixRealWorld_web/live/article_live/show.ex b/lib/phoenixRealWorld_web/live/article_live/show.ex index e770bf2..f58d743 100644 --- a/lib/phoenixRealWorld_web/live/article_live/show.ex +++ b/lib/phoenixRealWorld_web/live/article_live/show.ex @@ -15,8 +15,8 @@ defmodule PhoenixRealWorldWeb.ArticleLive.Show do article = Blogs.get_article!(id) %{live_action: action, current_user: user} = socket.assigns # 編集モードで、ログインユーザーが記事の作者でない場合は、記事一覧にリダイレクト - if action = :edit && article.author.id != user.id do - {:noreply, push_navigate(socket, to: ~p"/articles")} + if action == :edit && article.author_id != user.id do + {:noreply, push_navigate(socket, to: ~p"/articles/#{article}")} # 編集モードで、ログインユーザーが記事の作者である場合は、記事を表示 else changeset = Blogs.change_comment(%Comment{}) #←追加 @@ -24,16 +24,16 @@ defmodule PhoenixRealWorldWeb.ArticleLive.Show do {:noreply, socket |> assign(:page_title, page_title(socket.assigns.live_action)) - |> assign(:article, Blogs.get_article!(id))} - |> assign(:commment_form, to_form(changeset)) #←追加 + |> assign(:article, article) + |> assign(:comment_form, to_form(changeset))} #←追加 end end @impl true - def handle_event("delete", _params, socket) do + def handle_event("delete", _value, socket) do %{article: article, current_user: user} = socket.assigns - if article.author.id == user.id do + if article.author_id != user.id do {:noreply, socket} else {:ok, _} = Blogs.delete_article(article) diff --git a/lib/phoenixRealWorld_web/live/article_live/show.html.heex b/lib/phoenixRealWorld_web/live/article_live/show.html.heex index 2a7da6d..1321a87 100644 --- a/lib/phoenixRealWorld_web/live/article_live/show.html.heex +++ b/lib/phoenixRealWorld_web/live/article_live/show.html.heex @@ -1,8 +1,8 @@ <.header> <:subtitle>This is a article record from your database. - <:actions :if={@current_user && @current_user.id == @article.author.id}> - <.link patch={~p"/articles/#{@article}/edit"} phx-click={JS.focus()}> + <:actions :if={@current_user && @current_user.id == @article.author_id}> + <.link patch={~p"/articles/#{@article}/edit"} phx-click={JS.push_focus()}> <.link phx-click={JS.push("delete")} data-confirm="Are you sure?"> @@ -11,9 +11,6 @@ - - - <.list> <:item title="Title">{@article.title} <:item title="Body">{@article.body} diff --git a/lib/phoenixRealWorld_web/router.ex b/lib/phoenixRealWorld_web/router.ex index 0336d60..d01a1c4 100644 --- a/lib/phoenixRealWorld_web/router.ex +++ b/lib/phoenixRealWorld_web/router.ex @@ -94,7 +94,7 @@ defmodule PhoenixRealWorldWeb.Router do # 記事の一覧と詳細はログイン不要.ログインユーザーのデータも参照可能 live "/articles", ArticleLive.Index, :index #←追加 - live "/articles/:id", ArticleLive.Show :show #←追加 + live "/articles/:id", ArticleLive.Show, :show #←追加 end end end diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate new file mode 100644 index 0000000..28f8618 --- /dev/null +++ b/rel/overlays/bin/migrate @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +exec ./phoenixRealWorld eval PhoenixRealWorld.Release.migrate diff --git a/rel/overlays/bin/migrate.bat b/rel/overlays/bin/migrate.bat new file mode 100644 index 0000000..6b9fc85 --- /dev/null +++ b/rel/overlays/bin/migrate.bat @@ -0,0 +1 @@ +call "%~dp0\phoenixRealWorld" eval PhoenixRealWorld.Release.migrate diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server new file mode 100644 index 0000000..2888e84 --- /dev/null +++ b/rel/overlays/bin/server @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./phoenixRealWorld start diff --git a/rel/overlays/bin/server.bat b/rel/overlays/bin/server.bat new file mode 100644 index 0000000..ebc6665 --- /dev/null +++ b/rel/overlays/bin/server.bat @@ -0,0 +1,2 @@ +set PHX_SERVER=true +call "%~dp0\phoenixRealWorld" start diff --git a/test/phoenixRealWorld_web/live/article_live_test.exs b/test/phoenixRealWorld_web/live/article_live_test.exs index 00e1f80..253b4f0 100644 --- a/test/phoenixRealWorld_web/live/article_live_test.exs +++ b/test/phoenixRealWorld_web/live/article_live_test.exs @@ -13,8 +13,20 @@ defmodule PhoenixRealWorldWeb.ArticleLiveTest do %{article: article} end + # ↓追加 + defp create_article_with_tag(_) do + {:ok, %{article: article}} = + Blogs.insert_article_with_tag(%{ + title: "some title", + body: "some body", + author_id: user_fixture().id, + tags_string: "test" + }) + %{article_with_tag: article} + end + describe "Index" do - setup [:create_article] + setup [:create_article, :create_article_with_tag] # ←変更 test "lists all articles", %{conn: conn, article: article} do {:ok, _index_live, html} = live(conn, ~p"/articles") @@ -23,6 +35,18 @@ defmodule PhoenixRealWorldWeb.ArticleLiveTest do assert html =~ article.title end + test "searches articles by tag", %{conn: conn, article: article, article_with_tag: article_with_tag} do + {:ok, index_live, _html} = live(conn, ~p"/articles") + + index_live |> element("a", "test") |> render_click() + assert_redirect(index_live, ~p"/articles?tag=test") + + {:ok, _index_live, html} = live(conn, ~p"/articles?tag=test") + + refute html =~ "/articles/#{article.id}" + assert html =~ "/articles/#{article_with_tag.id}" + end + test "saves new article", %{conn: conn} do # {:ok, index_live, _html} = live(conn, ~p"/articles") #↓追加 diff --git a/test/support/fixtures/blogs_fixtures.ex b/test/support/fixtures/blogs_fixtures.ex index 9b7e1aa..f830561 100644 --- a/test/support/fixtures/blogs_fixtures.ex +++ b/test/support/fixtures/blogs_fixtures.ex @@ -15,8 +15,8 @@ defmodule PhoenixRealWorld.BlogsFixtures do attrs |> Enum.into(%{ body: "some body", - title: "some title" - authore_id: user_fixture().id #←追加 + title: "some title", + author_id: user_fixture().id #←追加 }) |> PhoenixRealWorld.Blogs.create_article() @@ -34,7 +34,7 @@ defmodule PhoenixRealWorld.BlogsFixtures do attrs |> Enum.into(%{ body: "some body", - article_id: article_fixture().id #←追加 + article_id: article_fixture().id, #←追加 author_id: user_fixture().id #←追加 }) |> PhoenixRealWorld.Blogs.create_comment()