От переводчика: «Elixir и Phoenix — прекрасный пример того, куда движется современная веб-разработка. Уже сейчас эти инструменты предоставляют качественный доступ к технологиям реального времени для веб-приложений. Сайты с повышенной интерактивностью, многопользовательские браузерные игры, микросервисы — те направления, в которых данные технологии сослужат хорошую службу. Далее представлен перевод серии из 11 статей, подробно описывающих аспекты разработки на фреймворке Феникс казалось бы такой тривиальной вещи, как блоговый движок. Но не спешите кукситься, будет действительно интересно, особенно если статьи побудят вас обратить внимание на Эликсир либо стать его последователями.
В этой части мы подготовим фундамент для комментариев, чтобы затем оживить их с помощью каналов Phoenix.
На данный момент наше приложение основано на:
- Elixir: v1.3.1
- Phoenix: v1.2.0
- Ecto: v2.0.2
На чём мы остановились
Сейчас движок поддерживает использование небольшого Markdown-редактора для украшения постов, так что наша поделка становится похожей на полноценный проект! Однако у нас по-прежнему нет никакого способа получать обратную связь к постам, которые пишем. Хорошая новость: добавить такую возможность довольно просто. Вся работа будет строиться на том, что мы уже успели сделать.
Начнём с простого. Вместо требования зарегистрироваться, будем создавать новые комментарии в статусе ожидания утверждения. Комментарии станут видны на странице поста сразу после проверки, либо при нажатии «Показать неподтверждённые».
Добавляем модель комментариев
Начнём с добавления модели для комментариев. У комментариев будут:
- Автор (строковый тип)
- Сообщение (текстовый тип)
- Признак одобренного комментария (логический тип, по умолчанию false)
- Пост, к которому относится комментарий (ссылка на таблицу с постами)
Нам не нужен полный набор шаблонов и остальные прелести, так что воспользуемся командой mix phoenix.gen.model
.
mix phoenix.gen.model Comment comments author:string body:text approved:boolean post_id:references:posts
Затем проведём миграцию:
mix ecto.migrate
Связываем комментарии с постами
В файле web/models/comment.ex
можно увидеть, что комментарии уже связаны с постами, но не хватает связи в обратную сторону.
Так что добавьте в определение схемы «posts» в файле web/models/post.ex
следующий код:
has_many :comments, Pxblog.Comment
После выполнения команды mix test
, всё должно быть по-прежнему зелёным! Теперь давайте проверим связь между постами и комментариями.
Для этого откройте файл test/support/factory.ex
и добавьте фабрику Comment. Напишите эту строчку наверх файла, следом за остальными псевдонимами:
alias Pxblog.Comment
А затем этот код в самый низ:
def comment_factory do
%Comment{
author: "Test User",
body: "This is a sample comment",
approved: false,
post: build(:post)
}
end
Соответственно, нужно создать несколько тестов, чтобы фабрика стала приносить пользу. Откройте файл test/models/comment_test.exs
и добавьте в него следующий код:
import Pxblog.Factory
# ...
test "creates a comment associated with a post" do
comment = insert(:comment)
assert comment.post_id
end
Запустим тесты снова. Они должны остаться зелёными!
Добавление маршрутов для комментариев
Начнём с создания основных маршрутов для комментариев. Откройте файл web/router.ex
:
resources "/posts", PostController, only: [] do
resources "/comments", CommentController, only: [:create, :delete, :update]
end
Комментарии имеют смысл только в контексте постов, так что вложим их внутрь. При этом, в других маршрутах, которые мы уже определили, посты вложены в пользователей! Не хочется создавать лишних маршрутов для постов, поэтому воспользуемся параметром only: []
. Затем добавим ресурсы для комментариев, чтобы дать возможность создавать, удалять и обновлять их. :create
— для добавления комментариев неавторизованными пользователями (создаются неподтверждёнными). :delete
— позволит автору поста и администраторам удалять комменарии, а :update
— одобрять их для показа общественности.
Добавляем контроллеры и представления
Теперь, когда наши модели настроены, нам нужно создать контроллер с парой методов. Показ комментариев будет реализован через контроллер постов, но создание/обновление/удаление должно быть реализовано в их собственном контроллере. Начнём с создания файла web/controllers/comment_controller.ex
:
defmodule Pxblog.CommentController do
use Pxblog.Web, :controller
end
Также создадим представление в файле web/views/comment_view.ex, как того хочет Phoenix.
defmodule Pxblog.CommentView do
use Pxblog.Web, :view
end
Теперь вернёмся назад в контроллер и добавим базовую структуру из трёх действий: create
, update
и delete
.
def create(conn, _), do: conn
def update(conn, _), do: conn
def delete(conn, _), do: conn
Затем нужно создать в новой директории шаблон формы добавления комментария, который разместим на странице показа поста:
$ mkdir web/templates/comment
Позволяем пользователям оставлять комментарии
Начнём с создания файла
web/templates/comment/form.html.eex:
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="form-group">
<%= label f, :author, class: "control-label" %>
<%= text_input f, :author, class: "form-control" %>
<%= error_tag f, :author %>
</div>
<div class="form-group">
<%= label f, :body, class: "control-label" %>
<%= textarea f, :body, class: "form-control", id: "body-editor" %>
<%= error_tag f, :body %>
</div>
<div class="form-group">
<%= submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>
Обычная формочка, тут нечего обсуждать.
Теперь перейдём к файлу web/templates/post/show.html.eex
, в который добавим связь с этой формой. Обратите внимание, что в этом шаблоне мы используем две переменные @changeset
и @action
. Мы вернёмся к этому позже в контроллере web/controllers/post_controller.ex
. А сейчас продолжим работать с шаблоном. После списка атрибутов поста, добавьте следующую строчку:
<%= render Pxblog.CommentView, "form.html", changeset: @comment_changeset, action: post_comment_path(@conn, :create, @post) %>
Нам нужно сослаться на «form.html» внутри представления CommentView
, так что передадим название первым аргументом в вызове render
. Нам нужно передать туда @comment_changeset
(который мы пока не определили, но скоро сделаем это) и @action
— путь для отправки комментариев.
Теперь можем перейти к файлу web/controllers/post_controller.ex
и сделать так, чтобы всё работало. Измените функцию show в соответствии с кодом:
def show(conn, %{"id" => id}) do
post = Repo.get!(assoc(conn.assigns[:user], :posts), id)
comment_changeset = post
|> build_assoc(:comments)
|> Pxblog.Comment.changeset()
render(conn, "show.html", post: post, comment_changeset: comment_changeset)
end
Теперь вернёмся к CommentController (файл web/controllers/comment_controller.ex
) и наполним содержимым функцию create. Прямо перед функциями добавьте следующий код:
alias Pxblog.Comment
alias Pxblog.Post
plug :scrub_params, "comment" when action in [:create, :update]
Включение update в вызов scrub_params пригодится нам позже. Теперь перепрыгнем к функции create и разместим в ней следующий код:
def create(conn, %{"comment" => comment_params, "post_id" => post_id}) do
post = Repo.get!(Post, post_id) |> Repo.preload([:user, :comments])
changeset = post
|> build_assoc(:comments)
|> Comment.changeset(comment_params)
case Repo.insert(changeset) do
{:ok, _comment} ->
conn
|> put_flash(:info, "Comment created successfully!")
|> redirect(to: user_post_path(conn, :show, post.user, post))
{:error, changeset} ->
render(conn, Pxblog.PostView, "show.html", post: post, user: post.user, comment_changeset: changeset)
end
end
Сейчас мы будем создавать комментарий при получении параметров comment_params и идентификатора поста post_id, так как они являются обязательными. Сначала забираем связанный пост (не забудьте предзагрузить пользователя и комментарии, так как шаблон скоро начнёт на них ссылаться), на основе которого создадим новый changeset. Для этого начиная с поста, идём по цепочке в функцию build_assoc
для создания связанной схемы, которую определяем через атом. В нашем случае создаётся связанный comment
. Результат вместе с comment_params передаём в функцию Comment.changeset. Остальная часть работает стандартно с одним исключением.
Условие ошибки чуть более сложное, потому что мы используем рендер другого представления. Сначала мы передаём соединение connection, затем связанное представление View (в нашем случае Pxblog.PostView
), шаблон для рендера и все переменные, используемые в шаблоне: @post
, @user
, and @comment_changeset
. Теперь можно протестировать: если отправить комментарий с ошибками, то вы увидите их список прямо на странице. Если при отправке комментария не будет никаких ошибок, вы получите синее flash-сообщение вверху страницы Мы делаем успехи!
Вывод комментариев
Теперь нам нужно отображать комментарии на странице поста. Для этого сформируем общий шаблон комментария, который можно использовать в любых местах для разных целей. Создайте файл web/templates/comment/comment.html.eex
и заполните его следующим:
<div class="comment">
<div class="row">
<div class="col-xs-4">
<strong><%= @comment.author %></strong>
</div>
<div class="col-xs-4">
<em><%= @comment.inserted_at %></em>
</div>
<div class="col-xs-4 text-right">
<%= unless @comment.approved do %>
<button class="btn btn-xs btn-primary approve">Approve</button>
<% end %>
<button class="btn btn-xs btn-danger delete">Delete</button>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<%= @comment.body %>
</div>
</div>
</div>
Здесь всё понятно без объяснений. Кнопки одобрить/удалить пока не подключены. Мы будем решать этот вопрос в следующих частях. Нам также нужно изменить контроллер для предзагрузки комментариев, и включить в шаблон постов show сам список комментариев. Начнём с обновления контроллера. Добавьте строчку в функцию show
из файла web/controllers/post_controller.ex
сразу за строчкой с получением постов:
post = Repo.get!(assoc(conn.assigns[:user], :posts), id)
|> Repo.preload(:comments)
Таким образом мы обеспечим загрузку комментариев как частм поста. Наконец, откройте файл web/templates/post/show.html.eex
и добавьте раздел шаблона, отображающий комментарии:
<div class="comments">
<h2>Comments</h2>
<%= for comment <- @post.comments do %>
<%= render Pxblog.CommentView, "comment.html", comment: comment %>
<% end %>
</div>
Добавляем тесты контроллера
Нельзя останавливаться, пока некоторые условия не покрыты тестами. Нам нужно проверить функцию create на случай успеха и неуспеха, так как по любому из этих путей может пройти выполнение кода.
Создайте файл test/controllers/comment_controller_test.exs
и приступим:
defmodule Pxblog.CommentControllerTest do
use Pxblog.ConnCase
import Pxblog.Factory
@valid_attrs %{author: "Some Person", body: "This is a sample comment"}
@invalid_attrs %{}
setup do
user = insert(:user)
post = insert(:post, user: user)
{:ok, conn: build_conn(), user: user, post: post}
end
test "creates resource and redirects when data is valid", %{conn: conn, post: post} do
conn = post conn, post_comment_path(conn, :create, post), comment: @valid_attrs
assert redirected_to(conn) == user_post_path(conn, :show, post.user, post)
assert Repo.get_by(assoc(post, :comments), @valid_attrs)
end
test "does not create resource and renders errors when data is invalid", %{conn: conn, post: post} do
conn = post conn, post_comment_path(conn, :create, post), comment: @invalid_attrs
assert html_response(conn, 200) =~ "Oops, something went wrong"
end
end
Снова воспользуемся фабрикой Pxblog.Factory. Мы также установим две переменных модуля @valid_attrs
и @invalid_attrs
точно так же, как мы делали раньше. Добавим блок setup, внутри которого настроим пользователя по умолчанию и пост, с которым будем работать.
Начнём с теста на успешное добавление комментария. Отправляем POST-запрос на вложенный путь с действительными атрибутами и проверяем, что как и ожидалось сработало перенаправление, а комментарий был добавлен к посту.
Теперь сделаем то же самое, но с недействительными данными, и проверим что получили сообщение “Oops, something went wrong” в виде HTML. Готово!
Дальнейшие шаги
Мы заготовили отличный фундамент для комментариев, который определённо можно продолжать развивать. Например, мы по-прежнему не имеем возможности одобрять и удалять комментарии. В следующих нескольких частях мы ещё немного поработаем над улучшением комментариев перед тем, как переходить на систему живых комментариев на базе каналов Phoenix.
Заключение от Вуншей
За два месяца, что у нас было в этом году, удалось сделать первые шаги в популяризации Эликсира. Первым делом мы основали русскоязычное сообщество Wunsh.ru, для которого перевели на русский язык полтора десятка самых интересных статей об Эликсире и функциональном программировании.
В начале недели мы обновили сайт и выложили в общий доступ пять статей. Надеемся, они раззадорят вас и убедят попробовать язык. Например, написать простое приложение в зимние каникулы. Завтра мы разошлём подписчикам полный набор вышедших статей. Поэтому подпишитесь сегодня и приглашайте друзей. Будем рады такому подарку!
Следующий шаг проекта — написать серьёзное введение для новичков, перевести официальную документацию и подробно ответить на частые вопросы:
- С чего начать?
- Как развернуть проект?
- Каким редактором пользоваться?
- Какие задачи решать?
И другие…
Будущий год будет наполнен стремительным движением самого языка. Его внедрением в российские компании. О языке не просто будут знать, его начнут массово (насколько это возможно) использовать.
Спасибо за то, что заходите в наши материалы. Если вы — подписчик, то приглашайте друзей.
Всех с наступающим!
Автор: jarosluv