RDR3T > Многоуровневые комментарии и не только

июня 10, 2011  |  Published in Ruby on Rails, Ruby on Rails 3  |  29 Comments

До этого момента, наше приложение представляло собой хаос. В данной главе RubyDev Rails 3 Tutorial мы займемся тем, что придадим нашему приложению монолитный вид. Большая часть работы будет происходить в файлах представлений. Что конкретно в планах на выполнение:

1. Сделать так, чтобы наш проект имел вид более-менее похожий на настоящий блог.
2. Присоединить вывод комментариев к представлению поста.
3. Реализовать вложенные комментарии.

Хочу сказать,что эта глава будет самая насыщенная и сложная по сравнению с уже опубликованными главами, поэтому наберитесь терпения и по ходу чтения где-то записывайте все, что вам не понятно, чтобы после прочтения задать вопросы в комментариях. Не бойтесь показаться чайником, все мы в чем-то чайники и только задавая вопросы человек может стать профессионалом.

Оговорка: Поскольку в нашем приложении имеется модель User представляющая собой пользователей блога, которые могут писать посты и комментарии, но нет авторизации, регистрации и установки прав пользователей, мы будем использовать для всех публикаций некоторого пользователя current_user, который будет представлять собой просто заглушку. В следующей главе мы рассмотрим создание регистрации пользователей, авторизации и определение прав пользователей для работы с приложением.

Прежде, чем начнем работу с кодом, давайте словами опишем то, что должно делать наше приложение и как оно должно выглядеть. Для описания функций и внешнего вида приложения будем использовать постулаты, то есть короткие, лаконичные выражения, описания:

- Экшен Post#index должен выводить список всех имеющихся постов.
- Для current_user посты должны быть снабжены кнопками управления: редактирование и удаление.

- Экшен Post.show должен предоставлять единственный пост, под которым должны размещаться все комментарии к посту и форма добавления нового комментария.

- При удалении поста должны удаляться все ассоциируемые с ним комментарии (уже реализовано)

- При удалении пользователя должны удаляться все ассоциируемые с ним комментарии и посты (уже реализовано)

- При удалении комментария должны удаляться все вложенные комментарии.

- Экшен User#index должен выводить список пользователей и кнопки управления пользователями: редактировать, удалить для current_user.

- Экшен User#show должен выводить информацию об одном определенном пользователе, а под этой информацией, в две колонки должны выводиться ссылки на все посты пользователя и комментарии пользователя.

- Комментарии к посту должны выводиться в порядке их написания. Вложенные комментарии находятся под родительскими комментариями с небольшим отступом с левой стороны.

- Экшен Categories#show выводит список всех постов из выбранной категории.

Вот мы и описали все требования к нашему приложению, которые мы должны воплотить в данной главе в жизнь. Перед разработкой приложения, вы должны в достаточной степени четко представлять то, какую работу оно должно выполнять и каким образом. Кто-то отделывается таким описанием, как то, что приведено выше, кто-то использует специальные фреймворки типа RSpec для написания спецификаций, а кто-то вообще себя этим не утруждает. По моему скромному и не авторитетному мнению, спецификации не являются обязательными и не должны покрывать все приложение целиком. О спецификациях мы поговорим в специальной статья посвященной RSpec, а сейчас приступим к работе!

Для начала нам необходимо создать пользователя current_user, от имени которого мы будем публиковать все посты и комментарии. Сейчас, в моей базе данных содержится всего одна запись о пользователе, которая осталась с прошлых глав:


User.all
#=> [#<User id: 2, name: "Vladimir", lname: "Melnik", login: "admin", password: "12345", email: "egotraumatic@gmail.com", created_at: "2011-04-14 14:22:32", updated_at: "2011-04-14 14:22:32">]
ruby-1.9.2-p0 >

Давайте будем использовать этого пользователя в качестве current_user:


@current_user = User.first
#=> #<User id: 2, name: "Vladimir", lname: "Melnik", login: "admin", password: "12345", email: "egotraumatic@gmail.com", created_at: "2011-04-14 14:22:32", updated_at: "2011-04-14 14:22:32">

Для того, чтобы не помещать код создания пользователя в каждый контроллер, мы поместим его в ApplicationController — контроллер от которого наследуются все остальные контроллеры нашего приложениях. Он необходим для того, чтобы выносить в него общий для нескольких контроллеров код. В разработке очень важно придерживаться принципа DRY, он делает код читабельнее и позволяет быстрее вносить правки в код и писать для него тесты. Ниже приведен пример того, как должен выглядеть код вашего ApplicationController:


class ApplicationController < ActionController::Base
protect_from_forgery
layout 'application'
def initialize
super
@current_user = User.first
end
end

Для работы мы будем использовать ветку master. Не забудьте подмешать ветку categories к master, если вы не сделали этого в предыдущей главе.

$ git checkout master

1. Маршрутизация для корневого URL

Первым делом сделаем, чтобы при обращению к корню сайта (в нашем случае это http://localhost:3000) срабатывал экшен Post#index. Для этого нам необходимо выполнить два действия:

1. Удалить файл ../public/index.html
2. Открыть файл настройки роутинга приложения ../config/routes.rb и добавить туда следующую строку кода:


root :to => "posts#index"

Файл ../public/index.html выдается по умолчанию при обращению к корневому адресу сайта. это происходит потому, что у директории /public приоритет выше, чем у контроллеров. Удаление файла ../public/index.html позволит приложению выдать другую страницу, чем стандартную страницу приветствия Rails приложения.

Метод root определяет какой экшен необходимо показать пользователю при обращении пользователя к новому адресу сайта. Строка «posts#index» расшифровывается как ИмяКонтроллера#ИмяЭкшена — это нововведение в Rails 3, которое используется вместо :controller => «posts», :action => «index».

Теперь при обращении к корневой (домашней) странице нашего проекта вы увидите представление экшена Posts#index.

2. Знакомство с layout’ами (макетами).

Layout — макет приложения. Он хранит базовую структуру страниц приложения в которую встраиваются шаблоны — представления отдельных экшенов. Макет — является еще одним примером применения принципа DRY, поскольку в код макета мы должны стараться вынести весь общий код для всех шаблонов — представлений экшенов. В данном разделе главы мы напишем простой макет, который бы делал наше приложение похожим на блог.

Все макеты приложений хранятся в директории ../app/views/layouts/. По умолчанию мы имеем макет application.html.erb, который по умолчанию используется для рендеринга представлений всех страниц. Вы также можете определить специфичный макет для определенного контроллера.

Открыв макет application.html.erb вы увидите следующий ERB код:


<!DOCTYPE html>
<html>
<head>
<title>Blog</title&gt
<%= stylesheet_link_tag :all %>
<%= javascript_include_tag :defaults %>
<%= csrf_meta_tag %>
</head>
<body>
<%= yield %>
</body>
</html>

ERB — представляет собой язык разметки, основанный на встраиваемых в обычный HTML код кусков кода на Ruby.

Включение шаблона в макет происходит при помощи <%= yield %>, не сложно догадаться, что шаблон передается в макет в виде блока кода, который вызывается при помощи выражения yield, и результат выполнения которого встает в место вызова.

Наш блог будет иметь двух-колоночный макет, а также меню в низу «шапки» и  название в шапке «RubyDev Blog Engine (RDBE)». Для простоты, наша колонка с меню, находящаяся справа, будет просто дублировать ссылки из меню находящегося под шапкой.

В примере кода из ApplicationController мы использовали следующее выражение: layout ‘application’. Метод layout используется для определения того, какой макет будет использоваться для данного контроллера. В нашем случае это будет макет …/app/views/layouts/application.html.erb. Наша задача отредактировать его таким образом, чтобы он был похож на макет блога. Все файлы с описанием стилей CSS, должны находиться в директории ../public/stylesheets и включаются в макет нашего приложения при помощи следующего выражения:  <%= stylesheet_link_tag :all %>.

Ниже приведен код макета application.html.erb и style.css (scaffold.css мы использовать не будем и удаляем.):

/* application.html.erb */
<!DOCTYPE html>
<html>
<head>
<title>RubyDev Blog Engine (RDBE)</title>
<%= stylesheet_link_tag :all %>
<%= javascript_include_tag :defaults %>
<%= csrf_meta_tag %>
</head>
<body>
<div id="container">
<div id="header">
<h1>RubyDev Blog Engine (RDBE)</h1>
</div>
<div id="navigation">
navigation here
</div>
<div id="content-container">
<div id="content">
<%= yield %>
</div>
<div id="right-bar">
right-bar here
</div>
<div id="footer">
Copyright © RubyDev.ru
</div>
</div>
</div>
</body>
</html>

/* style.css */
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;}
body {
margin:0;
padding:0;
background-color:#111}
#container {
margin: 0 auto;
width: 960px;
background: #eee;}
#header {
background: #111;
padding: 20px;}
#header h1 { margin: 0; color:#fefefe}
#navigation {
float: left;
width:100%;
background: #222;}
#content-container {
float: left;
width: 960px;
background-color:#eee;}
#content {
clear: left;
float: left;
width: 560px;
padding: 20px 0;
margin: 20px;
padding: 20px;
/*display: inline;*/
background-color:#fff;}
#content h2 { margin: 0; }
#right-bar{
float: right;
width: 260px;
margin: 20px 20px 20px 0;
padding: 20px;
/*display: inline;*/
background-color:#222;
color: #ddd;}
#right-bar h3 { margin: 0; }
#footer {
clear: left;
background: #000;
text-align: right;
padding: 20px;
height: 1%;
color:#eee;}

Теперь давайте займемся оформлением шаблонов для экшенов, и начнем мы с шаблонов для PostsController:


#posts/index.html.erb
<% @posts.each do |post| %>
<h2>
<%= link_to post.title, post %>
</h2>
<% if @current_user == post.user %>
<%= link_to 'Edit', edit_post_path(post) %>
<%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %>
<br />
<% end %>
<%= post.content %>
<br />
<b>Posted by</b> <%= link_to post.user.login, post.user %>
<hr />
<% end %>
<br />
<%= link_to 'New Post', new_post_path %>
#posts/new.html.erb
<h1>New post</h1>
<b>User:</b> <%= @current_user.login %>
<%= render 'form' %>
<%= link_to 'Back', posts_path %>

Из partial _form.html.erb удаляем поле для указания id пользователя:


#posts/_form.html.erb
<%= form_for(@post) do |f| %>
<% if @post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% @post.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :title %><br />
<%= f.text_field :title %>
</div>
<div>
<%= f.label :content %><br />
<%= f.text_area :content %>
</div>
<div>
<%= f.submit %>
</div>
<% end %>

Вместо него мы несколько переписываем экшен  PostsController#create:


def create
@post = Post.new(params[:post])
@post.user = @current_user
@post.save ? redirect_to(@post, :notice => 'Post was successfully created.') : render(:action => "new")
end

Теперь автором каждого поста будет тот пользователь, которого в ApplicationController мы назначили переменной @current_user.


#posts/show.html.erb
<p id="notice"><%= notice %></p>
<h1> <%= @post.title %> </h1>
<b>Posted by:</b> <%= link_to @post.user.login, @post.user %> |
<% if @current_user == @post.user%>
<%= link_to 'Edit', edit_post_path(@post) %>
<% end %>
<br />
<%= @post.content %>
<br />
<%= link_to 'Back', posts_path %>

Теперь давайте разберемся с представлениями для UsersController:


#users/inidex.html.erb
<% @users.each do |user| %>
<div class = 'user'>
<b>User name:</b> <%= user.name %> <%= user.lname %> (<%= user.login %>)
<%= link_to 'Show', user %>
<%= link_to 'Edit', edit_user_path(user) %>
<%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %>
</div>
<% end %>
<br />
<%= link_to 'New User', new_user_path %>

Мы также добавим пару строк описания стилей в public/stylesheets/style.css:


div.user{
border:1px solid #dfdfdf;
padding:5px;
margin:0 0 5px;}

Редактировать представления для CategoriesController мы не будем, так как в дальнейшем мы откажемся от этого контроллера. Управление категориями будет относится к админ-части приложения.

Следующее, чем мы займемся будет вывод комментариев относящихся к посту под постом, а также добавление возможности создания вложенных комментариев. Прежде, чем мы этим займемся, давайте разберемся с некоторыми полезными методами, которые мы использовали в коде представлений.

#link_to — данный метод используется для добавления ссылки. Первым аргументом он принимает текст ссылки, вторым — адрес ссылки, а третьим — хэш параметров.

#form_for — метод,  конструктор форм для экземпляра модели.

#edit_post_path, #new_post_path, #posts_path — динамически генерируемые методы под каждую модель, которые позволяют генерировать url для соответственно редактирования экземпляра модели, создания нового экземпляра модели, перехода к списку всех экземпляров модели (экшен index).

#redirect_to — метод, который позволяет выполнить редирект.

3. Добавление комментариев
Делать представление для комментариев самих по себе не имеет смысла, разве что если это не панель администрирования, о которой пока речи не идет. Комментарий не отделим от поста, которому он принадлежит, поэтому и представление комментариев должно происходить на странице поста которому они принадлежат, именно по этой причине мы не будем создавать представления для комментариев, а создадим partial _comment.html.erb в директории ../app/views/posts/, который будет отвечать за представление одного единственного комментария.

Ниже приведен код partial’а _comment.html.erb:


<div id = 'comments'>
<h3><%= @post.comments.count %> Comments</h3>
<hr />
<% @post.comments.each do |comment|%>
<div class = 'comment'>
<b>from</b> <%= link_to comment.user.login, comment.user %> >
<b><%=comment.title %></b>
<% if comment.user == @current_user %>
[
<%= link_to 'edit', edit_comment_path(comment) %>
<%= link_to 'delete', comment, :confirm => 'Are you sure?', :method => :delete %>
]
<% end %>
<br />
<%= comment.comment %>
</div>
<% end %>
</div>

Для того, чтобы использовать partial в контексте представления экшена PostsController#show, нам необходимо воспользоваться методом render в представлении экшена, для включения partial:


<p id="notice"><%= notice %></p>
<h1> <%= @post.title %> </h1>
<b>Posted by:</b> <%= link_to @post.user.login, @post.user %> |
<% if @current_user == @post.user%>
<%= link_to 'Edit', edit_post_path(@post) %>
<% end %>
<br />
<%= @post.content %>
<br />
<%= link_to 'Back', posts_path %>
<%= render 'comments' %>

render принимает строку ‘comments’, которая соответствует имени файла partial’а (_comments.html.erb) без расширения файла и символа подчеркивания. Обратите на это соглашение об именовании свое внимание!

Для того, чтобы наши комментарии выглядели более симпатично, давайте добавим в наш файл описания стилей пару новых правил:


div#comments{
background-color:#eee;
padding:20px;}
div.comment{
margin:0 0 5px;
padding:5px;
border:1px solid #dfdfdf;
background-color:#fff;}

Теперь, когда комментарии отображаются рядом с постом, нам необходимо добавить форму добавления нового комментария под списком всех комментариев поста. Для этого мы также будем использовать partial который будет храниться в файле _comments_form.html.erb, хотя с таким же успехом мы могли бы поместить его и в partial _comments.html.erb. Для создания формы для добавления комментария, нам необходимо иметь экземпляр комментария. Поскольку размещать бизнес-логику в представлениях считается плохим тоном, то создание экземпляра комментария нам необходимо выполнить в контексте PostsController:


def show
@post = Post.find(params[:id])
@comment = Comment.new
end

Поскольку комментарий не может не иметь автора и пост к которому он относится, то нам необходимо автоматически заполнять эти данные при отправке формы с комментарием или при обработке отправленных данных в контроллере. Мы воспользуемся скрытыми полями html форм для указания id поста и автора которым принадлежит комментарий. Это не самое лучшее решение, поэтому в дальнейшем его мы переделаем. Ниже приведен код partial’а _comments_form.html.erb:


<%= form_for(@comment) do |f| %>
<% if @comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@comment.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% @comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :title %><br />
<%= f.text_field :title %>
</div>
<div>
<%= f.label :comment %><br />
<%= f.text_area :comment, :size => "67x5"%>
</div>
<%= f.hidden_field :post_id, :value => @post.id %>
<%= f.hidden_field :user_id, :value => @current_user.id %>
<div>
<%= f.submit %>
</div>
<% end %>

Быстрое знакомство с hidden_field: выражение f.hidden_field :post_id, :value => @post.id используется для создания скрытого поля и присвоения ему значения @post.id, которое затем будет присвоено @comment.post_id. Вот html код, который был сгенерирован этим выражением: <input id=»comment_post_id» name=»comment[post_id]» type=»hidden» value=»3″ />.

Для того, чтобы при сохранении отредактированного комментария мы перемещались к посту, а не к списку всех комментариев (экшен index CommentsController) нам необходимо отредактировать экшен update в CommentsController следующим образом:


def update
@comment = Comment.find(params[:id])
if @comment.update_attributes(params[:comment])
redirect_to(@comment.post, :notice => 'Comment was successfully updated.')
else
render :action => "edit"
end
end

Теперь редирект будет происходить на страницу поста которому принадлежит комментарий.

Также нам необходимо удалить экшены index и show, которые нам не нужны, поскольку список всех комментариев нам не нужен, показ одного комментария нам также не нужен. Окончательный вид CommentsController приведен ниже:


class CommentsController < ApplicationController
def new
@comment = Comment.new
end
def edit
@comment = Comment.find(params[:id])
end
def create
@comment = Comment.new(params[:comment])
if @comment.save
redirect_to(@comment.post, :notice => 'Comment was successfully created.')
else
redirect_to(@comment.post, :notice => 'Error! Comment was\'nt created.')
end
end
def update
@comment = Comment.find(params[:id])
if @comment.update_attributes(params[:comment])
redirect_to(@comment.post, :notice => 'Comment was successfully updated.')
else
render :action => "edit"
end
end
def destroy
@comment = Comment.find(params[:id])
@comment.destroy
redirect_to(post_path(@comment.post))
end
end


4. Маленькие украшательства

Теперь давайте добавим меню и немного подправим код представлений и код CSS стилей.

Код приложения вы можете скачать здесь. Я не буду приводить в статье код CSS и обновленных представлений, это слишком большой объем кода, который не представляет интереса для вашего изучения программирования на платформе Rails, кроме того,если вам интересно, вы можете увидеть все исходные коды в GitHub репозитории или скачать в zip архиве.

Вместо второстепенных подробностей того, как я постарался украсить блог, мы рассмотрим добавление содержимого в правую колонку.

Как правило, в боковую колонку добавляется список всех категорий. Давайте сейчас реализуем его! Список категорий со ссылками будем реализован в виде partial ../app/views/layouts/_categories_list.html.erb:


<div id = 'categories-list'>
<h1 style='color:white;'>Categories</h1>
<% @categories.each do |category| %>
#Дла каждой категории создаем ссылку с названием категории и количеством постов
<%= link_to "#{category.name} #{category.count}", category_path(category), :class => 'category-link' %> <br />
<% end %>
</div>

Этот partial мы включаем в макет layouts/application.html.erb, поскольку он должен отображаться на всех страницах блога. Для включения partial в код представлений, мы помещаем следующий код в макет:


<div id="right-bar">
<%= render 'layouts/categories_list' %>
</div>

Обратите внимание на то, что мы явно указываем адрес местоположения partial потому, что по умолчанию partial ищется в директории представлений для текущего контроллера.

Вам наверное интересно, откуда мы берем используемую в partial переменную @categories. Переменную @categories мы определяем в инициализаторе ApplicationController — контроллера, от которого наследуются все контроллеры нашего приложения. Мы объявляем @categories в ApplicationController потому, что мы придерживаемся принципа DRY (Don’t Repeat Yourself) и единожды объявленная в ApplicationController переменная будет доступна во всех унаследованных контроллерах:


class ApplicationController < ActionController::Base
protect_from_forgery
layout 'application' #рендерим макет aplication.html.erb
def initialize
super
@current_user = User.first
@categories = Category.all
end
end

Если сейчас взглянуть на наше приложение, то можно увидеть,что у нас имеется 2 категории, которые тем не менее не имеют ни одного поста. Категории на самом деле не пусты, пусто поле posts_counter, которое проектировалось для хранения количества постов. Для того, чтобы в таблице с категориями для каждой категории записывалось количество постов, нам необходимо чтобы при каждом добавлении нового поста, происходило наращивание счетчика (поля count) категорий к которым относится добавленный пост. Для этого следует использовать такую  штуку, как callback. Callback переводится как «обратный вызов», но переводный вариант не прижился, поэтому все используют оригинальный термин «callback» («коллбек»). Callback — это механизм выполнения определенных действий после, перед или во время выполнения других действий. В нашем случае, нам необходимо после сохранение нового поста в базу данных увеличивать значение поля count в соответствующих категориях на единицу.

В Rails имеется набор callback’ов, но нам в данной ситуации пригодится лишь один из них -  after_create. Данный callback выполняется автоматически после сохранения новой записи. Почему мы увеличиваем posts_counter только после добавления нового поста, а не перед его добавлением? Это связано с тем, что при сохранении поста может возникнуть ошибка и пост не будет сохранен, при этом счетчик будет увеличен и будет показывать неправильное количество записей в категории.


class CategoryPost < ActiveRecord::Base
belongs_to :post
belongs_to :category
after_create :posts_counter_plus
after_destroy :posts_counter_minus
private
def posts_counter_plus
Category.find(self.category_id)
.update_attribute(:posts_counter, category.posts_counter + 1)
end
def posts_counter_minus
Category.find(self.category_id)
.update_attribute(:posts_counter, category.posts_counter - 1)
end
end

Только что мы добавили в модель CategoryPost два коллбека для увеличения и уменьшения счетчика постов.

Добавление возможности пометки категорий при публикации поста

Что нам осталось сделать с постами и категориями? — Нам осталось добавить возможность выбирать категории, которым будет принадлежать пост. Сделать это достаточно просто, ниже приведен алгоритм того, как это делается:

1. Получаем коллекцию всех категорий
2. Поскольку пост может принадлежать нескольким категориям, то дополняем форму создания поста коллекцией именно checkbox’ов для каждой категории. Таким образом пользователь может пометить категории которым будет принадлежать пост.
3. Полученные из формы данные обрабатываем в контроллере.

Начинаем с контроллера PostsController, где немножко дополним экшен new:


def new
@post = Post.new
end

Экшен new создает объект — пост, по которому будет создаваться форма, данные из которой будут переданы в экшен create, который создаст новый экземпляр поста с данными полученными из экшена new и сохранит его. В коде выше мы объявили переменную @post - пустой экземпляр поста для генерации формы создания поста. Также нам доступна переменная @categories — коллекция всех категорий которую мы будем использовать для того, чтобы выводить список категорий вместе с соответствующими им чекбоксами для того, чтобы можно было отметить какой категории будет принадлежать пост.

Для того, чтобы построить список категорий с чекбоксами нам необходимо познакомиться с новым методом — хэлпером форм: check_box_tag, но пока я приведу пример кода формы:


<%= form_for(@post) do |f| %>
<% if @post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% @post.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :title %><br />
<%= f.text_field :title %>
</div>
<div>
<%= f.label :content %><br />
<%= f.text_area :content %>
</div>
<div>
<% @categories.each do |category| %>
<div>
<%= check_box_tag 'post[category_ids][]', category.id, @post.category_ids.include?(category.id) %>
<%= label 'post', 'category_ids', h(category.name), :value => category.id %>
</div>
<% end %>
</div>
<div>
<%= f.submit %>
</div>

Здесь мы перебираем значения из коллекции @categories и для каждого создаем чекбокс и подпись к нему. Код:


<div>
<% @categories.each do |category| %>
<div>
<%= check_box_tag 'post[category_ids][]', category.id, @post.category_ids.include?(category.id) %>
<%= label 'post', 'category_ids', category.name, :value => category.id %>
</div>
<% end %>
</div>

генерирует следующий HTML код формы для экшена new:


<div>
<div>
<input id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="1" />
<label for="post_category_ids_1">Ruby</label>
</div>
<div>
<input id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="2" />
<label for="post_category_ids_2">Rails</label>
</div>
<div>
<input id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="3" />
<label for="post_category_ids_3">JavaScript</label>
</div>
<div>
<input id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="4" />
<label for="post_category_ids_4">Programming</label>
</div>
</div>

и следующий для экшена edit:


<div>
<div>
<input checked="checked" id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="1" />
<label for="post_category_ids_1">Ruby</label>
</div>
<div>
<input checked="checked" id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="2" />
<label for="post_category_ids_2">Rails</label>
</div>
<div>
<input checked="checked" id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="3" />
<label for="post_category_ids_3">JavaScript</label>
</div>
<div>
<input checked="checked" id="post_category_ids_" name="post[category_ids][]" type="checkbox" value="4" />
<label for="post_category_ids_4">Programming</label>
</div>
</div>

Последний аргумент, а именно выражение: @post.category_ids.include?(category.id), отвечает за то, будет ли чекбокс по умолчанию отмечен или нет. Если значение true, или любое, которое оценивается как true, то чекбокс будет по умолчанию отмечен, если false или nil, то чекбокс не будет отмечен. По умолчанию чекбоксы не отмечены. Первый и второй аргументы задают соответственно свойства name и value для элемента checkbox формы. Свойства name со значениями post[category_ids][] и свойства value со значениями category.id, в контроллере доступны как params[:post]["category_ids"], то есть в виде массива id категорий. params[:post] будет содержать набор значений необходимых для создания поста, которые хранятся под ключами соответствующими аксессорам экземпляра поста, пример:

@post = Post.new(params[:post])

и


@post = Post.new{"title"=>"Post title here", "content"=>"Post content here", "category_ids"=>["1"]}

эквивалентны.

Многоуровневые комментарии

Теперь, когда с постами покончено, следует заняться созданием ветвящихся комментариев. В маленьких блогах ветвящиеся комментарии — сомнительная роскошь, однако мы рассчитываем на то, что на нашем движке будет работать что-то вроде хабра или lookatme, по этому ветвящиеся комментарии будут полезны.

Для реализации ветвящихся или многоуровневых комментариев мы будем использовать какие штуки как рекурсия и полиморфизм.

Рекурсия — это когда функция или метод вызывают сами себя напрямую, или через другую функцию или метод.

Еще раз напомню, что такое полиморфизм. Полиморфизм — «многоформие» — это когда одна сущность может притворяться другой. В нашем случае полиморфны будут как сами комментарии, так и посты. Все они будут притворяться какой-то особенной сущностью Commentable (что-то, что может быть комментируемо). Таким образом комментарии второго уровня уже не относятся в посту, а относятся к комментариям второго уровня, а комментарии третьего уровня к комментариям второго и так далее.

Давайте вспомним, в каких отношениях между собой пребывают наши категории и посты:

Комментарии: belongs_to :post
Посты: has_many :comments, :dependent => :destroy

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


#app/models/comment.rb
class Comment < ActiveRecord::Base
validates :comment, :presence => true
validates :user_id, :presence => true
validates :commentable_id, :presence => true
belongs_to :user
belongs_to :commentable, :polymorphic => true
has_many :comments, :as => :commentable
end
#app/models/post.rb
class Post < ActiveRecord::Base
validates :user_id, :presence => true
validates :title, :presence => true
validates :content, :presence => true
belongs_to :user
has_many :category_posts, :dependent => :destroy
has_many :categories, :through => :category_posts
has_many :comments, :as => :commentable, :dependent => :destroy
end

Обратите внимание на строку belongs_to :commentable, :polymorphic => true, а точнее на параметр :polimorphic, именно он включает полиморфизм. Благодаря ему в таблице comments проверяется значение поля commentable_type, которое хранит тип комментируемой сущности, в нашем случае post или comment. Нам необходимо хранить тип потому, что id у постов и комментарием может совпадать и не будет ясно, посту или комментарию принадлежит комментарий. Кроме того, поле post_id в таблице comments нам больше не нужно, ведь комментарии теперь не принадлежат на прямую постам, а принадлежат некоему объекту commentable; по этому мы должны избавиться от post_id и добавить поле commentable_id. У каждой записи в базе данных должен быть уникальный ключ, такой, чтобы с его помощью можно было бы получить значение одной единственной записи. Поскольку commentable_id не является таким ключом, как например post_id или comment_id, то нам необходимо создать такой ключ. Для создания такого ключа в миграции следует использовать метод add_index, который должен создать ключ из двух полей commentable_id и commentable_type. Комбинируя эти два поля в одном ключе мы достигаем его уникальности.

Для создания миграции воспользуемся специальным генератором миграций:

$ rails g migration AddPolimorphicComments
invoke  active_record
create    db/migrate/20110509163600_add_polimorphic_comments.rb

Наша миграция будет выглядеть следующим образом:


class AddPolimorphicComments < ActiveRecord::Migration
def self.up
drop_table :comments
create_table :comments do |t|
t.string :title
t.text :comment
t.integer :user_id
t.integer :commentable_id
t.string :commentable_type
t.timestamps
end
add_index :comments, [:commentable_id, :commentable_type]
end
def self.down
drop_table :comments
create_table :comments do |t|
t.string :title
t.text :comment
t.integer :user_id
t.integer :post_id
t.timestamps
end
end
end

Благодаря этой миграции мы можем не только создать новую таблицу comments, но и вернуться к старой версии базы данных.

$ rake db:migrate
(in /home/vladimir/proj/blog)
==  AddPolimorphicComments: migrating =========================================
— drop_table(:comments)
-> 0.0444s
— create_table(:comments)
-> 0.0030s
— add_index(:comments, [:commentable_id, :commentable_type])
-> 0.0037s
==  AddPolimorphicComments: migrated (0.0515s) ================================

Теперь, когда полиморфные связи реализованы, мы можем присваивать одному комментарию другие комментарии, давайте запустим консоль и проверим так ли это:


post = Post.first
=> #<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">
comment = Comment.new do |c|
c.user_id = 2
c.title = "Comment Title"
c.comment = "Comment text"
end
#=> #<Comment id: nil, title: "Comment Title", comment: "Comment text", user_id: 2, commentable_id: nil, commentable_type: nil, created_at: nil, updated_at: nil>
post.comments << comment
#=> [#<Comment id: 1, title: "Comment Title", comment: "Comment text", user_id: 2, commentable_id: 3, commentable_type: "Post", created_at: "2011-05-09 18:25:29", updated_at: "2011-05-09 18:25:29">]
post.save #=> true
post.comments
#=> [#<Comment id: 1, title: "Comment Title", comment: "Comment text", user_id: 2, commentable_id: 3, commentable_type: "Post", created_at: "2011-05-09 18:25:29", updated_at: "2011-05-09 18:25:29">]
comment
#=> #<Comment id: 1, title: "Comment Title", comment: "Comment text", user_id: 2, commentable_id: 3, commentable_type: "Post", created_at: "2011-05-09 18:25:29", updated_at: "2011-05-09 18:25:29">
nested_comment = Comment.new do |c|
c.title = "Comment title"
c.user_id = 3
c.comment = "Nested comment text"
end
#=> #<Comment id: nil, title: "Comment title", comment: "Nested comment text", user_id: 3, commentable_id: nil, commentable_type: nil, created_at: nil, updated_at: nil>
comment.comments << nested_comment
#=> [#<Comment id: 2, title: "Comment title", comment: "Nested comment text", user_id: 3, commentable_id: 1, commentable_type: "Comment", created_at: "2011-05-09 18:28:32", updated_at: "2011-05-09 18:28:32">]
comment.save #=> true

Теперь нам необходимо заняться представлением комментариев. По моей задумке, div с вложенным комментарием должен иметь свойства стилей flot: right и иметь ширину на 20px меньше, чем ширина div родительского комментария. Это можно реализовать в два способа:

1. Вычисляем ширину каждого div исходя из уровня его вложенности.
2. Помещать дочерние комментарии в div родительских.

Лично мне больше нравится второй вариант, поскольку он исключает необходимость свойства style для каждого div c комментарием или использование специального JavaScript для оформления комментариев.

Для того, чтобы создать ветвящуюся структуру комментариев нам понадобится два partial’a, первый _comments.html.erb для добавления в разметку страницы контейнера для всех комментариев и информации о количестве комментариев, а также для рендеринга комментариев первого уровня, и второй _comment.html.erb для рендеринга всех остальных комментариев. Ниже приведен код обоих partial’ов:


#_comments.html.erb
<div id = 'comments'>
<h3><%= @post.comments.count %> Comments</h3>
<hr />
<%= render :partial => "comment", :collection => @post.comments %>
</div>
#_comment.html.erb
<div>
<div>
<b><%= comment.title %></b><br />
posted by <%= link_to comment.user.login, comment.user %>,
posted at: <%= comment.created_at.to_formatted_s(:short)%>
</div>
<%= comment.comment %>
<%= render :partial => "comment", :collection => comment.comments %>
</div>

Хочется обратить ваше внимание на параметр :collection метода render. Этот параметр позволяет производить рендеринг коллекции, другими словами, мы не используем render в цикле перебирая комментарии, но используем перебор комментариев внутри render.

Следующее, что нам необходимо сделать, это исправить некоторые оплошности:

1. Сделать правильный вывод количество комментариев у поста (сейчас учитываются только комментарии первого уровня).

2. Реализовать редирект при добавлении вложенного комментария, который не принадлежит посту непосредственно.

Для решения этих двух задач я представляю одно единственное решение:

В форму добавления комментария должен передаваться параметр comment_post_id, а также должен срабатывать after_create фильтр в модели комментарием, который соответствующему посту увеличит значение счетчика.

Кроме того, нам необходимо добавить возможность создавать вложенные комментарии!
Для создания вложенных комментариев нам необходимо добавить к каждому комментарию ссылку типа «ответить» которая переносила бы нас к форме создания комментария, для которой устанавливались соответствующие значения commentable_id и commentable_type.

Давайте начнем с правильного редиректа после добавления комментария! Для этого вам необходимо будет немного отредактировать экшен create в контроллере CommentsController и partial с формой комментариев. Ниже приведен код экшена create и обновленный код формы:


#../app/views/posts/_comments_form.html.erb
<%= form_for(@comment) do |f| %>
<% if @comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@comment.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% @comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :title %><br />
<%= f.text_field :title %>
</div>
<div>
<%= f.label :comment %><br />
<%= f.text_area :comment, :size => "67x5"%>
</div>
<% unless params[:comment] && params[:comment][:commentable_id] && params[:comment][:commentable_type] %>
<%= f.hidden_field :commentable_id, :value => @post.id %>
<%= f.hidden_field :commentable_type, :value => "Post" %>
<% else %>
<%= f.hidden_field :commentable_id, :value => params[:comment][:commentable_id] %>
<%= f.hidden_field :commentable_type, :value => params[:comment][:commentable_type] %>
<% end %>
<%= hidden_field_tag :post_id, params[:post_id] ? params[:post_id] : @post.id %>
<%= f.hidden_field :user_id, :value => @current_user.id %>
<div>
<%= f.submit %>
</div>
<% end %>
#CommentsController#create
def create
@comment = Comment.new(params[:comment])
if @comment.save
redirect_to(post_path(params[:post_id]), :notice => 'Comment was successfully created.')
else
redirect_to(post_path(params[:post_id]), :notice => 'Error! Comment was\'nt created.')
end
end

Как видите, мы несколько изменили форму комментариев заменив post_id на commentable_id и commentable_type, и добавив строку   <%= hidden_field_tag :post_id, params[:post_id] ? params[:post_id] : @post.id %>, благодаря которой мы передаем ID поста, к которому относится комментарий напрямую или через родительский комментарий. Данные из поля post_id используются только для правильного редиректа на страницу поста!

Теперь займемся решением задачи добавления вложенных комментариев. Для начала добавим ссылку «ответить» или «reply» к каждому комментарию в partial _comment.html.erb:


<div>
<div>
<b><%= comment.title %></b><br />
posted by <%= link_to comment.user.login, comment.user %>,
posted at: <%= comment.created_at.to_formatted_s(:short)%>
</div>
<%= comment.comment %>
<%= link_to "reply", new_comment_path({:comment => {:commentable_id => comment.id, :commentable_type => "Comment"}, :post_id => @post.id}) %>
<%= render :partial => "comment", :collection => comment.comments %>
</div>

А также отредактируем экшен new в контроллере CommentsController:


#CommentsController#new
def new
@comment = Comment.new(params[:comment])
@comment.user = @current_user
end

Также отредактируем шаблон ../app/views/comments/new.html.erb, чтобы оно использовало имеющийся во vievs/comments/ layout для создания формы:


<h1>New comment</h1>
<%= render :partial => "/posts/comments_form" %>
<%= link_to 'Back', comments_path %>

Теперь мы имеем супер крутую многоуровневую систему комментирования=) Осталось только реализовать правильный подсчет комментариев! Для этого добавим в таблицу с постами новое поле — comments_count, которое будет хранить количество комментариев:

$ rails g migration AddCommentsCountToPost
invoke  active_record
create    db/migrate/20110601211729_add_comments_count_to_post.rb

Открываем файл миграции и забиваем в него следующий простой код:


class AddCommentsCountToPost < ActiveRecord::Migration
def self.up
add_column :posts, :comments_counter, :integer
end
def self.down
remove_column :posts, :comments_counter
end
end

Запускаем нужный rake-task:

$ rake db:migrate
(in /home/vladimir/proj/blog)
==  AddCommentsCountToPost: migrating =========================================
— add_column(:posts, :comments_counter, :integer)
-> 0.0013s
==  AddCommentsCountToPost: migrated (0.0014s) ================================

… и необходимое поле готово!

Давайте теперь удалим все имеющиеся комментарии, чтобы в будущем показывалось правильное их количество. В консоли Rails наберите следующий код:


Comment.destroy_all

Теперь комментарии удалены. Для того, чтобы при добавлении новых комментариев происходило наращивание счетчика, мы используем колбек-метод after_create, который после добавления комментария будет наращивать счетчик комментариев на единицу. Вот так будет выглядеть наша модель Comment после добавления в нее коллбека after_create:


class Comment < ActiveRecord::Base
validates :comment, :presence => true
validates :user_id, :presence => true
validates :commentable_id, :presence => true
belongs_to :user
belongs_to :commentable, :polymorphic => true
has_many :comments, :as => :commentable
def after_create
@post = comments_post(self)
@post.update_attribute(:comments_counter, @post.comments_counter + 1)
end
def comments_post(comment)
if comment.commentable_type == "Post"
return comment.commentable
else
return comments_post(comment.commentable)
end
end
end

Метод #comments_post возвращет пост, которому напрямую или через другие комментарии принадлежит текущий комментарий.

Что еще необходимо сделать? — Нам необходимо сделать так, чтобы при удалении комментария счетчик комментариев уменьшался, кроме того, вместе с комментарием должны удаляться все вложенные относящиеся к нему комментарии. Для этого воспользуется коллбеком after_destroy. Для начала давайте добавим ссылку на удаление комментария для большего удобства проверки того, все ли хорошо работает:


<div>
<div>
<b><%= comment.title %></b><br />
posted by <%= link_to comment.user.login, comment.user %>,
posted at: <%= comment.created_at.to_formatted_s(:short)%>
<% if comment.user == @current_user %>
<%= link_to("delete", comment_path(comment, { :post_id => @post.id }), :method => :delete) %>
<% end %>
</div>
<%= comment.comment %>
<%= link_to "reply", new_comment_path({:comment => {:commentable_id => comment.id, :commentable_type => "Comment"}, :post_id => @post.id}) %>
<%= render :partial => "comment", :collection => comment.comments %>
</div>

Теперь откроем модель Comment и пропишем зависимость для ассоциации has_many :comments:


has_many :comments, :as => :commentable, :dependent => :destroy

После этого, при удалении комментария будут удаляться все вложенные комментарии и для каждого из них все вложенные в них комментарии.

Когда задача с удалением цепочек постов решена, давайте займемся задачей уменьшения значения comments_counter после удаления комментария. Для этого мы воспользуемся коллбеком after_destroy. После всех правок наша модель Comment будет иметь следующий вид:


class Comment < ActiveRecord::Base
validates :comment, :presence => true
validates :user_id, :presence => true
validates :commentable_id, :presence => true
belongs_to :user
belongs_to :commentable, :polymorphic => true
has_many :comments, :as => :commentable, :dependent => :destroy
def after_create
@post = comments_post(self)
@post.update_attribute(:comments_counter, @post.comments_counter + 1)
end
def after_destroy
@post = comments_post(self)
@post.update_attribute(:comments_counter, @post.comments_counter - 1)
end
def comments_post(comment)
if comment.commentable_type == "Post"
return comment.commentable
else
return comments_post(comment.commentable)
end
end
end

Я не могу их оставить в таком виде в котором они сейчас находятся, хочется сделать их красивее, по этому открываем файл стилей и файлы представлений, добавляем тегам необходимые классы и прописываем необходимые стили.


#фрагмент ../public/stylesheets/style.css
div#comments{
background-color:#eee;
padding:20px;}
div.comment{
margin:0 -1px 10px 0;
padding:10px 0 0 10px;
border:1px solid #dfdfdf;
background-color:#fff;}
div.comment-header{
background-color: #f9f9f9;
margin: -10px 0 0 -10px;
padding:10px 0 10px 10px;
color: #ccc;
font-size: .8em;
border-bottom: 1px solid #dfdfdf;
}
div.comment-text{
margin: 0;
padding: 10px 0;
}


#../app/view/posts/_comment.html.erb
<div>
<div>
<b><%= comment.title %></b><br />
posted by <%= link_to comment.user.login, comment.user %>,
posted at: <%= comment.created_at.to_formatted_s(:short)%>
<% if comment.user == @current_user %>
<%= link_to("delete", comment_path(comment, { :post_id => @post.id }), :method => :delete) %>
<% end %>
</div>
<div>
<%= comment.comment %>
<br />
<%= link_to "reply", new_comment_path({:comment => {:commentable_id => comment.id, :commentable_type => "Comment"}, :post_id => @post.id }) %>
</div>
<%= render :partial => "comment", :collection => comment.comments %>
</div>

В следующей статье мы создадим самопальную систему авторизации и регистрации, пользовательские роли и Ajax’сифицируем наше приложение?. Спасибо за внимание!

Примечание: Статья и код блога писались не параллельно, поэтому в статье могут присутствовать отличия от реального кода. Кроме того, все писалось наспех, поэтому в статье и коде могут быть ошибки и оплошности. Например неправильно названная миграция  AddPolimorphicComments, которую правильнее было бы назвать AddNestedComments или еще как-нибудь по-другому. Тем не менее код работает. Смотрите код в репозитории на .

Лучшая благодарность автору — ваши комментарии.

Есть проблемы? — Задавайте вопросы!

Tags: ,

Responses

  1. says:

    июня 10, 2011 at 23:50 (#)

    s/Зделать/Сделать/

  2. admin says:

    июня 11, 2011 at 00:47 (#)

    proton, спасибо, очень сильно торопился, подоздеваю, что ошибок миллион. Сейчас у меня сессия и в то же время не хочется останавливать публикации.

  3. says:

    июня 11, 2011 at 16:39 (#)

    Хорошо и доходчиво показана работа полиморфных моделей, хотя мне кажется, что вложенные комментарии лучше было бы сделать через acts_as_nested_set, что помогло бы избежать манипуляций с after_ калбеками.

    И, если не ошибаюсь, аггрегаторы Post.comments_count работают «искаропки» без необходимсти использования калбеков, при наличии соответствующего поля в базе.

    Подробнее здесь

    вот такой инструкцией включается:
    belongs_to :post, :counter_cache => true

  4. admin says:

    июня 11, 2011 at 22:03 (#)

    mprokopov, вы правы, в rails имеется встроенный счетчик принадлежащих сущностей, однако он не всегда подходит, например для многоуровневых комментариев он не подходит, а для постов в категориях подходит отлично. Однако мне больше нравится работать с коллбеками, хотя это и не rails-way. В следущей статье в ходе рефакторинга, возможно, перепишу все это с counter_cache.

  5. releu says:

    июня 12, 2011 at 10:40 (#)

    
      
        
      
     
      
      
       'Are you sure?', :method => :delete %>
    
    

    лучше через render писать, а права через cancan, линк для поста можно писать дефолтный:

    #post.rb
    
    def to_s
      title
    end
    
    # template
    = render @posts
    
    # partial _post.html.erb
    
    
    
      #... links
    
    

    Лучше использовать
    current_user.posts.build(params[:post])

    Все не читал, автору советую пройти прежде чем учить новичков. По rails-way идти надо, потому что только так все просто получается, а сворачивать надо через красивые расширения возможностей.

    Тогда все будет и красиво и понятно :)

  6. admin says:

    июня 12, 2011 at 10:56 (#)

    releu, будет специальная статья по рефакторингу, когда блог более-менее наполнится функционалом.

  7. ip82 says:

    июня 12, 2011 at 11:43 (#)

    Оформите репу на гитхабе пожалуйста, хоть readme поменяйте… а то нажмешь watch и потом непонятно что за проект, по названию догадывайся :-) По статье — получилась большая и хорошая :-)

  8. ip82 says:

    июня 12, 2011 at 11:48 (#)

    Да, немного непонятно — не раскрыто содержание папки spec, а это неотъемлемая часть разработки)) Хотя если вы не хотели раздувать статью…

  9. Alexey says:

    июля 21, 2011 at 16:16 (#)

    Когда мы пишем код для добавления комментария, мы создаём partial _comment.html.erb. В коде же мы ссылаемся на файл render ‘commentS’. Нехорошо.

  10. admin says:

    июля 22, 2011 at 12:43 (#)

    Alexey, спасибо, поправил.

  11. EVil says:

    августа 23, 2011 at 08:13 (#)

    NoMethodError in PostsController#create
    undefined method `posts_counter' for "#":Category

    Откуда взялось поле posts_counter. Мы создавали в таблице Categories поле count для хранения количества постов в категории.

  12. EVil says:

    августа 23, 2011 at 09:23 (#)

    Не дождался ответа и сделал вот так

      def posts_counter_plus
        @cats = Category.find(self.category_id)       
        if @cats.count.nil?     
          @catcount = 0
        else
          @catcount = category.count
        end
        @cats.update_attribute(:count, @catcount +1)
      end
    
      def posts_counter_minus
        @cats = Category.find(self.category_id)       
        if @cats.count.nil?     
          @catcount = 0
        else
          @catcount = category.count
        end
        @cats.update_attribute(:count, @catcount - 1)
      end
    
  13. admin says:

    августа 23, 2011 at 17:44 (#)

    EVil, спасибо за сообщение об ошибке! Дело в том, что статья и код писались не параллельно, по этому в коде были некоторые исправления, которых нет в статье и наоборот. Чуть позже поправлю.

  14. says:

    ноября 14, 2011 at 18:45 (#)

    >> Когда мы пишем код для добавления комментария, мы создаём partial _comment.html.erb. В коде же мы ссылаемся на файл render ‘commentS’. Нехорошо.

    Ну, вообще, можно render @comments

  15. vladiboc says:

    января 2, 2012 at 10:18 (#)

    Вот такое выдает при попытке посмотреть список коментов к посту:

    ActionView::MissingTemplate in Posts#show

    Showing /home/sysadmin/proj/blog/app/views/posts/show.html.erb where line #12 raised:

    Missing partial posts/comments, application/comments with {:handlers=>[:erb, :builder, :coffee], :formats=>[:html], :locale=>[:en, :en]}. Searched in:
    * «/home/sysadmin/proj/blog/app/views»

    Extracted source (around line #12):

    9:
    10:
    11:
    12:

    Rails.root: /home/sysadmin/proj/blog
    Application Trace | Framework Trace | Full Trace

    app/views/posts/show.html.erb:12:in `_app_views_posts_show_html_erb__21085319_86333860′

    Request

    Parameters:

    {«id»=>»3″}

  16. admin says:

    января 2, 2012 at 21:07 (#)

    vladiboc, в отчете об ошибке все сказано:

    ActionView::MissingTemplate in Posts#show
    Missing partial posts/comments, application/comments

    У вас отсутствует partial («фрагмент») с названием _comments.html.erb в app/views/posts/. В show.html.erb судя по всему он рендерится в 12 строке.

  17. vladiboc says:

    января 3, 2012 at 20:14 (#)

    Спасибо! Все получилось.
    Сразу не понял соглашение об именах

  18. vladiboc says:

    января 5, 2012 at 11:21 (#)

    NoMethodError in PostsController#destroy

    undefined method `posts_counter’ for #
    Вот такая штука выходит когда пытаюсь сделать Destroy поста:

    Rails.root: /home/sysadmin/proj/blog
    Application Trace | Framework Trace | Full Trace

    app/models/category_post.rb:17:in `posts_counter_minus’
    app/controllers/posts_controller.rb:37:in `destroy’

    Request

    Parameters:

    {«_method»=>»delete»,
    «authenticity_token»=>»nDi3Czuibmt9CQCnngCNwQbEwa/1ghbdaVRMXzH0P7k=»,
    «id»=>»3″}

  19. admin says:

    января 5, 2012 at 13:17 (#)

    vladiboc, в комментарие выше от EVil предложено возможное решение. Дело в том, что я в коде позже переименовал поле для подсчета постов в категории на posts_counter, а в статье это упустил. Лично я склоняюсь к следующему решению проблемы: нужно создать новую миграцию и переименовать в ней название поля или сделать роллбек миграций и в миграции поправить название.

    Вообще это цикл статей у меня не очень получился. Через неделю закончится сессия и я опубликую первую статью из нового цикла по Rails 3.1 — там уже таких глупых ошибок не будет.

  20. vladiboc says:

    января 6, 2012 at 11:56 (#)

    Спасибо! Исправил. Успешно сдать сессию!

  21. admin says:

    января 7, 2012 at 16:21 (#)

    vladiboc, спасибо!

  22. Yorka says:

    марта 20, 2012 at 10:49 (#)

    У меня почему-то кнопка Delete не работает нигде, что можно посмотреть?

  23. admin says:

    марта 20, 2012 at 12:16 (#)

    Yorka, button_to используете? Если да, то там по умолчанию post запрос шлется, потому нужно писать так:

    button_to ‘delete’, comment, :method => :delete

  24. Yorka says:

    марта 20, 2012 at 18:28 (#)

    С button_to нет проблем, все работает, а вот link_to не хочет работать
    ‘Are you sure?’, :method => :delete %>

  25. Yorka says:

    марта 22, 2012 at 18:27 (#)

    точнее вот такой код, почему-то редиректит на страницу едит, и совсем не удаляет то, что я хочу, как быть?
    [/ruby] ‘Are you sure?’, :method => :delete %>[/ruby]

  26. Yorka says:

    марта 22, 2012 at 18:29 (#)

    [/html] ‘Are you sure?’, :method => :delete %>[/html]

  27. Александр says:

    октября 18, 2012 at 15:30 (#)

    Я правильно понимаю, что что бы вывести пост с комментами, будет запросов в базу примерно 1 + количество вложенных комментов?

    То етсь имея такое дерево комментариев

    Пост (1)
    -комент (2)
    —коммент (3)
    —коммент (4)
    -комент (2)
    —коммент (5)
    —коммент (6)
    —-коммент (7)
    -комент (2)
    —коммент (8)

    будет 8 запросов в базу?

  28. Ali says:

    февраля 26, 2013 at 15:44 (#)

    да, проверено. не есть гуд

  29. Ali says:

    февраля 26, 2013 at 15:48 (#)

    и где же продолжение — Ajax’сифицируем.
    хотел бы посмотреть, как это Ajax’сифицируется.
    вроде пост старый…

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.