От переводчика: «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

Теперь вернёмся назад в контроллер и добавим базовую структуру из трёх действий: createupdate и 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, для которого перевели на русский язык полтора десятка самых интересных статей об Эликсире и функциональном программировании.

В начале недели мы обновили сайт и выложили в общий доступ пять статей. Надеемся, они раззадорят вас и убедят попробовать язык. Например, написать простое приложение в зимние каникулы. Завтра мы разошлём подписчикам полный набор вышедших статей. Поэтому подпишитесь сегодня и приглашайте друзей. Будем рады такому подарку!

Следующий шаг проекта — написать серьёзное введение для новичков, перевести официальную документацию и подробно ответить на частые вопросы: 

  • С чего начать?
  • Как развернуть проект?
  • Каким редактором пользоваться?
  • Какие задачи решать?

И другие…

Будущий год будет наполнен стремительным движением самого языка. Его внедрением в российские компании. О языке не просто будут знать, его начнут массово (насколько это возможно) использовать.

Спасибо за то, что заходите в наши материалы. Если вы — подписчик, то приглашайте друзей.

Всех с наступающим!
Поделиться с друзьями
-->

Комментарии (1)


  1. railsfun
    01.01.2017 07:18

    Спасибо, заинтересовали! Подписался.