Глава 11 Микросообщения пользователей
В Главе 10 были закончены REST действия для ресурса Users, так что пришло время наконец-то добавить второй ресурс: пользовательские микросообщения.1 Эти короткие сообщения, связанные с конкретным пользователем, впервые были показаны (в зачаточной форме) в Главе 2. В этой главе мы сделаем полноценную версию наброска из Раздела 2.3, сконструировав модель данных Micropost, связав ее с моделью User при помощиhas_many и belongs_to методов, а затем сделав формы и партиалы, необходимые для манипулирования результатами и их отображения. В Главе 12, мы завершим наш крохотный клон Twitter, добавив понятие слежения за пользователями, чтобы получить поток (feed) их микросообщений.Если вы используете Git для управления версиями, я предлагаю сделать новую тему ветки, как обычно:
$ git checkout -b user-microposts
11.1 Модель Micropost
Мы начнем Microposts ресурс с создания модели Micropost, которая фиксирует основные характеристики микросообщений. Что мы сделаем основываясь на работе, проделанной в Разделе 2.3; как и модель из того раздела, наша новая модель Micropost будет включать валидации и ассоциации с моделью User. В отличие от той модели, данная Micropost модель будет полностью протестирована, а также будет иметь дефолтное упорядочивание и автоматическую деструкцию в случае уничтожения родительского пользователя.11.1.1 Базовая модель
Модели Micropost необходимы лишь два атрибута:content атрибут, содержащий текст микросообщений,2 и user_id связывающий микросообщения с конкретным пользователем. Как и в случае с моделью User (Листинг 6.1), мы генерируем ее используя generate model:$ rails generate model Micropost content:string user_id:integer
microposts в базе данных (Листинг 11.1); сравните ее с аналогичной миграцией для таблицы users из Листинга 6.2.
Листинг 11.1. Миграция Micropost. (Обратите внимание на индекс
user_id.) db/migrate/<timestamp>_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration
def self.up
create_table :microposts do |t|
t.string :content
t.integer :user_id
t.timestamps
end
add_index :microposts, :user_id
add_index :microposts, :created_at
end
def self.down
drop_table :microposts
end
end
user_id и created_at:add_index :microposts, :user_id
add_index :microposts, :created_at
t.timestamps, которая (как указано в Разделе 6.1.1) добавляет волшебные столбцы created_at и updated_at. Мы будем работать со столбцом created_at в Разделе 11.1.3 и Разделе 11.2.1.Мы можем запустить миграцию микросообщений как обычно (с учетом необходимости подготовки тестовой базы данных, поскольку модель данных изменилась):
$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare
Рисунок 11.1: Модель данных Micropost.
Доступные атрибуты
Прежде чем конкретизировать модель Micropost, важно в первую очередь применитьattr_accessible для указания атрибутов, редактируемых посредством веб. Как обсуждалось в Разделе 6.1.2.2 и Разделе 10.4.1.1,
неопределение доступных атрибутов означает что кто-нибудь может легко
изменить любой аспект объекта микросоообщений используя клиент командной
строки для выдачи вредоносного запроса. Например, злоумышленник может
изменить user_id атрибуты на микросообщениях, тем самым связав микросообщения с неправильным пользователем.В случае модели Micropost, есть только один атрибут, который необходимо редактировать через веб, а именно, атрибут
content (Листинг 11.2).
Листинг 11.2. Открытие доступа к атрибуту
content (и только к content атрибуту). app/models/micropost.rb
class Micropost < ActiveRecord::Base
attr_accessible :content
end
user_id не внесен как attr_accessible параметр, он не может быть редактирован через веб, поэтому user_id параметр при массовом назначении, таком какMicropost.new(:content => "foo bar", :user_id => 17)
Декларация
attr_accessible в Листинге 11.2 необходима для безопасности сайта, но она представляет проблему для дефолтного Micropost model spec (Листинг 11.3).
Листинг 11.3. Начальный Micropost spec.
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
before(:each) do
@attr = {
:content => "value for content",
:user_id => 1
}
end
it "should create a new instance given valid attributes" do
Micropost.create!(@attr)
end
end
Проблема в том, что
before(:each) блок в Листинге 11.3 назначает id пользователя через массовое назначение, собственно, что attr_accessible и предотвращает; в частности, как отмечалось выше, :user_id => 1 часть хэша инициализации просто игнорируется. Решением заключается в избегании использования непосредственно Micropost.new; вместо этого, мы будем создавать микросообщения через их ассоциацию с моделью User, которая устанавливает id пользователя автоматически. Достижение этого является задачей следующего раздела.11.1.2 Ассоциации Пользователь/Микросообщения
Цель этого раздела заключается в установлении ассоциации (связи) между моделями Micropost и User — отношений, кратко рассмотренных в Разделе 2.3.3 и показанных схематически в Рис. 11.2 и Рис. 11.3. По пути, мы будем писать тесты для модели Micropost которые, в отличие от тестов в Листинге 11.3, будут совместимы сattr_accessible из Листинга 11.2.
Рисунок 11.2:
belongs_to взаимоотношение между микросообщением и создавшим его пользователем. (полный размер)
Рисунок 11.3:
has_many отношение между пользователем и его микросообщениями. (полный размер)Micropost.create! тест показанный в Листинге 11.3 без несостоятельного массового назначения. Во-вторых, мы видим из Рис. 11.2 что объект micropost должен иметь метод user. Наконец, micropost.user должен быть пользователем, соответствующим user_id микросообщений. Мы можем выразить эти требования в RSpec кодом Листинга 11.4.
Листинг 11.4. Тесты для ассоциации микросообщений пользователя.
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
before(:each) do
@user = Factory(:user)
@attr = { :content => "value for content" }
end
it "should create a new instance given valid attributes" do
@user.microposts.create!(@attr)
end
describe "user associations" do
before(:each) do
@micropost = @user.microposts.create(@attr)
end
it "should have a user attribute" do
@micropost.should respond_to(:user)
end
it "should have the right associated user" do
@micropost.user_id.should == @user.id
@micropost.user.should == @user
end
end
end
Micropost.create или Micropost.create! для создания микросообщения, Листинг 11.4 использует@user.microposts.create(@attr)
@user.microposts.create!(@attr)
user_id с правильно установленным значением, что устраняет проблему, отмеченную в Разделе 11.1.1.1. В частности, код before(:each) do
@attr = {
:content => "value for content",
:user_id => 1
}
end
it "should create a new instance given valid attributes" do
Micropost.create!(@attr)
end
:user_id => 1 ничего не делает, когда user_id не является одним из доступных атрибутов модели Micropost. Пройдя через ассоциацию пользователя, с другой стороны, кодit "should create a new instance given valid attributes" do
@user.microposts.create!(@attr)
end
user_id по своей конструкции.Эти специальные
create методы пока не будут работать; они требуют надлежащей has_many ассоциации в модели User. Мы отложим более детализированные тесты для этой ассоциации до Раздела 11.1.3; на данный момент мы просто протестируем на наличие атрибута microposts (Листинг 11.5).
Листинг 11.5. Тест для атрибута пользовательских
microposts. spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
describe "micropost associations" do
before(:each) do
@user = User.create(@attr)
end
it "should have a microposts attribute" do
@user.should respond_to(:microposts)
end
end
end
belongs_to/has_many ассоциацию, проиллюстрированную в Рис. 11.2 и Рис. 11.3, как показано в Листинге 11.6 и Листинге 11.7.
Листинг 11.6. Микросообщения
belongs_to (принадлежат) пользователю. app/models/micropost.rb
class Micropost < ActiveRecord::Base
attr_accessible :content
belongs_to :user
end
Листинг 11.7. Пользователь
has_many (имеет много) микросообщений. app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :email, :password, :password_confirmation
has_many :microposts
.
.
.
end
belongs_to/has_many ассоциации, Rails строит методы, показанные в Таблице 11.1. Вы можете сравнить записи в Таблице 11.1 с кодом в Листинге 11.4 и Листинге 11.5 чтобы убедиться, что вы понимаете основные свойства ассоциаций. (В Таблице 11.1 есть один метод, который мы прежде не использовали — метод build; ему найдется хорошее применение в Разделе 11.1.4 и особенно в Разделе 11.3.2.)| Метод | Назначение |
|---|---|
micropost.user | Возвращает объект User связанный с микросообщением. |
user.microposts | Возвращает массив микросообщений пользователя. |
user.microposts.create(arg) | Создает микросообщение (user_id = user.id). |
user.microposts.create!(arg) | Создает микросообщение (с исключением в случае неудачи). |
user.microposts.build(arg) | Возвращает объект new Micropost (user_id = user.id). |
Таблица 11.1: Резюме методов ассоциации user/micropost.
11.1.3 Улучшение микросообщений
Тестыhas_many ассоциации в Листинге 11.5 мало чего тестируют — они просто проверяют существование атрибута microposts. В этом разделе мы добавим упорядочивание и зависимость к микросообщениям, а также протестируем что user.microposts метод действительно возвращает массив микросообщений.Нам нужно будет построить несколько микросообщений в User model spec, что означает, что мы должны сделать фабрику микросообщений в этой точке. Для этого нам нужен способ для создания ассоциации в Factory Girl. К счастью, это легко — мы просто используем Factory Girl метод
micropost.association, как показано в Листинге 11.8.3
Листинг 11.8. Полный файл фабрики, включающий новую фабрику для микросообщений.
spec/factories.rb
# By using the symbol ':user', we get Factory Girl to simulate the User model.
Factory.define :user do |user|
user.name "Michael Hartl"
user.email "mhartl@example.com"
user.password "foobar"
user.password_confirmation "foobar"
end
Factory.sequence :email do |n|
"person-#{n}@example.com"
end
Factory.define :micropost do |micropost|
micropost.content "Foo bar"
micropost.association :user
end
Дефолтное упорядочивание
Мы можем поместить фабрику в тест упорядочивания микросообщений. По умолчанию, использованиеuser.microposts
для вытягивания пользовательских микросообщений из базы данных не дает
никаких гарантий сохранения порядка микросообщений, но мы хотим (следуя
конвенции блогов и Twitter), чтобы микросообщения выдавались в обратном
хронологическом порядке, т.е. последнее созданное сообщение должно быть
первым в списке. Для проверки этого порядка мы сначала создаем пару
микросообщений следующим образом: @mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
@mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
1.hour.ago (один.час.назад), а второй пост был создан 1.day.ago
(один.день.назад). Обратите внимание, насколько удобна Factory Girl в
использовании: мы можем не только назначать пользователя используя
массовое назначение (так как фабрики пренебрегают attr_accessible), мы также можем установить created_at вручную, чего не позволяет делать Active Record.4Большинство адаптеров баз данных (в том числе адаптер SQLite) возвращает микросообщения в порядке их id, поэтому мы можем организовать начальные тесты, которые почти наверняка провалятся, используя код в Листинге 11.9.
Листинг 11.9. Тестирование порядка пользовательских микросообщений.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
describe "micropost associations" do
before(:each) do
@user = User.create(@attr)
@mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
@mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
end
it "should have a microposts attribute" do
@user.should respond_to(:microposts)
end
it "should have the right microposts in the right order" do
@user.microposts.should == [@mp2, @mp1]
end
end
end
@user.microposts.should == [@mp2, @mp1]
[@mp1, @mp2]. Эти тесты также тестируют базовую корректность самой has_many ассоциации, проверяя (как указано в Таблице 11.1) что user.microposts является массивом микросообщений.Для того чтобы получить прохождение тестов упорядоченности, мы используем Rails средство
default_scope с параметром :order, как показано в Листинге 11.10. (Это наш первый пример понятия пространства. Мы узнаем о пространстве в более общем контексте в Главе 12.)
Листинг 11.10. Упорядочивание микросообщений с
default_scope. app/models/micropost.rb
class Micropost < ActiveRecord::Base
.
.
.
default_scope :order => 'microposts.created_at DESC'
end
’microposts.created_at DESC’, где DESC это SQL для “по убыванию”, т.е., в порядке убывания от новых к старым.Dependent: destroy
Помимо правильного упорядочивания, есть второе уточнение, которое мы хотели бы добавить в микросообщения. Напомним из Раздела 10.4 что администраторы сайта имеют право уничтожать пользователей. Само собой разумеется, что если пользователь уничтожен, то должны быть уничтожены и его микросообщения. Мы можем протестировать это вначале уничтожив пользователя, а затем проверив, что связанных с ним микросообщений больше нет в базе данных (Листинг 11.11).
Листинг 11.11. Тестирование того, что микросообщения уничтожаются вместе с пользователями.
spec/models/user_spec.rb
describe User do
.
.
.
describe "micropost associations" do
before(:each) do
@user = User.create(@attr)
@mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
@mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
end
.
.
.
it "should destroy associated microposts" do
@user.destroy
[@mp1, @mp2].each do |micropost|
Micropost.find_by_id(micropost.id).should be_nil
end
end
end
.
.
.
end
Micropost.find_by_id, который возвращает nil если запись не найдена, в то время как Micropost.find вызывает исключение в случае возникновения ошибки, что немного сложнее для проверки. (В случае, если вам интересно, тоlambda do
Micropost.find(micropost.id)
end.should raise_error(ActiveRecord::RecordNotFound)
Код приложения, необходимый для прохождения тестов из Листинга 11.11 короче чем одна строка; в самом деле, это всего лишь опция метода ассоциации
has_many, как показано в Листинге 11.12.
Листинг 11.12. Обеспечение уничтожения микросообщений пользователя вместе с пользователем.
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
has_many :microposts, :dependent => :destroy
.
.
.
end
11.1.4 Валидации микросообщений
Прежде чем покинуть модель Micropost, мы наведем в ней окончательный лоск, добавив валидации (следуя примеру из Раздела 2.3.2). Иuser_id и content атрибуты должны существовать, а content в дальнейшем вынужден быть не длиннее чем 140 знаков, что мы можем протестировать, используя код в Листинге 11.13.
Листинг 11.13. Тесты для валидаций модели Micropost.
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
before(:each) do
@user = Factory(:user)
@attr = { :content => "value for content" }
end
.
.
.
describe "validations" do
it "should require a user id" do
Micropost.new(@attr).should_not be_valid
end
it "should require nonblank content" do
@user.microposts.build(:content => " ").should_not be_valid
end
it "should reject long content" do
@user.microposts.build(:content => "a" * 141).should_not be_valid
end
end
end
Как и в Разделе 6.2, код в Листинге 11.13 использует мультипликацию строки для тестирования валидации длины микросообщения:
$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
new конструктора как вUser.new(...)
build:@user.microposts.build
Micropost.new, кроме того, что он автоматически устанавливает user_id к @user.id.Сами валидации являются прямыми аналогами валидаций модели User, как видно из Листинга 11.14.
Листинг 11.14. Валидации модели Micropost.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
attr_accessible :content
belongs_to :user
validates :content, :presence => true, :length => { :maximum => 140 }
validates :user_id, :presence => true
default_scope :order => 'microposts.created_at DESC'
end
11.2 Просмотр микросообщений
Хотя у нас еще нет способа создания микросообщений через веб — он появится лишь в Разделе 11.3.2 — это не остановит нас от их отображения (и тестирования этого отображения). Следуя по стопам Twitter мы запланируем отображение микросообщений пользователя не на отдельной странице micropostsindex, а непосредственно на самой странице user show, как показано на макете в Рис. 11.4.
Мы начнем с довольно простых ERb шаблонов для добавления отображения
микросообщений в профиле пользователя, а затем мы добавим микросообщения
в заполнителя образцов данных из Раздела 10.3.2 чтобы у нас было что отображать.
Рисунок 11.4: Макет страницы профиля с микросообщениями. (полный размер)
11.2.1 Дополнение страницы показывающей пользователя
Начнем с теста для отображения микросообщений пользователя. Мы работаем в Users controller spec, так как контроллер Users содержит действие usershow. Наша стратегия заключается в создании
пары "фабричных" микросообщений, связанных с пользователем, а затем
проверке, что страница show имеет тег span с CSS классом "content" содержащим текст каждого микросообщения. Результирующий RSpec пример представлен в Листинге 11.15.
Листинг 11.15. Тесты для отображения микросообщений на странице user
show. spec/controllers/users_controller_spec.rb
require 'spec_helper'
describe UsersController do
render_views
.
.
.
describe "GET 'show'" do
before(:each) do
@user = Factory(:user)
end
.
.
.
it "should show the user's microposts" do
mp1 = Factory(:micropost, :user => @user, :content => "Foo bar")
mp2 = Factory(:micropost, :user => @user, :content => "Baz quux")
get :show, :id => @user
response.should have_selector("span.content", :content => mp1.content)
response.should have_selector("span.content", :content => mp2.content)
end
end
.
.
.
end
Листинг 11.16. Добавление микросообщений в страницу user
show. app/views/users/show.html.erb
<table class="profile">
<tr>
<td class="main">
.
.
.
<% unless @user.microposts.empty? %>
<table class="microposts" summary="User microposts">
<%= render @microposts %>
</table>
<%= will_paginate @microposts %>
<% end %>
</td>
<td class="sidebar round">
<strong>Name</strong> <%= @user.name %><br />
<strong>URL</strong> <%= link_to user_path(@user), @user %><br />
<strong>Microposts</strong> <%= @user.microposts.count %>
</td>
</tr>
</table>
table через мгновение, но есть
несколько других вещей, которые необходимо отметить в первую очередь.
Одна из идей заключается в использовании empty? в строке@user.microposts.empty?
empty?, мы видели прежде в контексте строк (например, Раздел 4.2.3), к массиву:$ rails console
>> [1, 2].empty?
=> false
>> [].empty?
=> true
unless,<% unless @user.microposts.empty? %>
Вы также могли отметить в Листинге 11.16 что мы превентивно добавили пагинацию для микросообщений.
<%= will_paginate @microposts %>
<%= will_paginate %>
will_paginate предполагает существование переменной экземпляра @users (которая, как мы видели в Разделе 10.3.3, должна принадлежать классу WillPaginate::Collection). В данном случае, поскольку мы все еще в контроллере Users, но хотим пагинировать микросообщения, а не пользователей, мы явно передаем @microposts переменную в will_paginate. Конечно, это означает, что мы должны будем определить такую переменную в user show действии (Листинг 11.18 below).Наконец, отметим, что мы воспользовались этой возможностью, чтобы добавить графу текущего количества микросообщений к сайдбару профиля:
<td class="sidebar round">
<strong>Name</strong> <%= @user.name %><br />
<strong>URL</strong> <%= link_to user_path(@user), @user %><br />
<strong>Microposts</strong> <%= @user.microposts.count %>
</td>
@user.microposts.count является аналогом метода User.count,
за исключением того, что он рассчитывает количество микросообщений,
принадлежащих данному пользователю через ассоциацию
пользователь/микросообщение.6Теперь о самой microposts
table:<table class="microposts" summary="User microposts">
<%= render @microposts %>
</table>
<%= render @users %>
@users, используя партиал _user.html.erb. Аналогичным образом, код<%= render @microposts %>
_micropost.html.erb (наряду с директорией представлений micropost), как показано в Листинге 11.17.
Листинг 11.17. Партиал для отображения отдельно взятого микросообщения.
app/views/microposts/_micropost.html.erb
<tr>
<td class="micropost">
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
</td>
</tr>
time_ago_in_words, чей эффект мы увидим в Разделе 11.2.2.До сих пор, несмотря на определение всех соответствующих ERb шаблонов, тесты в Листинг 11.15 Листинге 11.15 должны быть провальными за неимением переменной
@microposts. Мы можем заставить их пройти с кодом из Листинга 11.18.
Листинг 11.18. Добавление переменной экземпляра
@microposts в user show действие. app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def show
@user = User.find(params[:id])
@microposts = @user.microposts.paginate(:page => params[:page])
@title = @user.name
end
end
paginate — он работает даже с ассоциацией микросообщений, конвертируя массив в объект WillPaginate::Collection на лету.После добавления CSS из Листинга 11.19 к нашему
custom.css файлу,7 мы можем взглянуть на нашу новую страницу профиля пользователя в Рис. 11.5. Это довольно… печально. Конечно, это связано с тем, что в настоящее время у нас нет микросообщений. Пришло время изменить это.
Листинг 11.19. CSS для микросообщений (включает в себя все CSS для этой главы).
public/stylesheets/custom.css
.
.
.
h1.micropost {
margin-bottom: 0.3em;
}
table.microposts {
margin-top: 1em;
}
table.microposts tr {
height: 70px;
}
table.microposts tr td.gravatar {
border-top: 1px solid #ccc;
vertical-align: top;
width: 50px;
}
table.microposts tr td.micropost {
border-top: 1px solid #ccc;
vertical-align: top;
padding-top: 10px;
}
table.microposts tr td.micropost span.timestamp {
display: block;
font-size: 85%;
color: #666;
}
div.user_info img {
padding-right: 0.1em;
}
div.user_info a {
text-decoration: none;
}
div.user_info span.user_name {
position: absolute;
}
div.user_info span.microposts {
font-size: 80%;
}
form.new_micropost {
margin-bottom: 2em;
}
form.new_micropost textarea {
height: 4em;
margin-bottom: 0;
}
Рисунок 11.5: Страница профиля пользователя с кодом для микросообщений, но без микросообщений. (полный размер)
11.2.2 Образцы микросообщений
При всей проделанной работе по созданию шаблонов для микросообщений пользователя в Разделе 11.2.1, конец был довольно разочаровывающим. Мы можем исправить эту печальную ситуацию, добавив микросообщения во вноситель образцов данных из Раздела 10.3.2. Добавление образцов микросообщений для всех пользователей займет довольно много времени, поэтому сначала мы выберем только первые шесть пользователей8 используя:limit опцию метода User.all:9User.all(:limit => 6)
Faker::Lorem.sentence возвращает текст lorem ipsum; как отмечалось в Главе 6, lorem ipsum имеет увлекательную предысторию.) Результирующий новый вноситель образцов данных показан в Листинге 11.20.
Листинг 11.20. Добавление микросообщений к образцам данных.
lib/tasks/sample_data.rake
namespace :db do
desc "Fill database with sample data"
task :populate => :environment do
.
.
.
User.all(:limit => 6).each do |user|
50.times do
user.microposts.create!(:content => Faker::Lorem.sentence(5))
end
end
end
end
db:populate:$ bundle exec rake db:populate
time_ago_in_words из Листинга 11.17.
Если вы подождете пару минут и перезагрузите страницу, вы увидите, как
текст автоматически обновится в соответствии с новым временем.
Рисунок 11.6: Профиль пользователя (/users/1) с микросообщениями. (полный размер)
Рисунок 11.7: Профиль другого пользователя, тоже с микросообщениями (/users/3). (полный размер)
Рисунок 11.8: Вторая страница профиля с микросообщениями, с пагинационными ссылками (/users/1?page=2). (полный размер)
11.3 Манипулирование микросообщениями
Закончив моделирование данных и шаблоны для отображения микросообщений, сейчас мы обратим наше внимание на интерфейс для их создания через веб. Результатом будет наш третий пример использования формы HTML, для создания ресурса — в данном случае, ресурса Microposts.11 В этом разделе мы также увидим первый намек на status feed — понятие, полной реализацией которого мы займемся в Главе 12. Наконец, как и с пользователями, мы сделаем возможным уничтожение микросообщений через веб.Существует один разрыв с предыдущими соглашениями, который стоит отметить: интерфейс ресурса Microposts будет работать главным образом за счет контроллеров Users и Pages, а не полагаться на собственный контроллер. Это означает, что маршруты для ресурса Microposts необычайно просты, как показано в Листинге 11.21. Код в Листинге 11.21 в свою очередь приводит к RESTful маршрутам показанным в Таблице 11.2, которые являются сокращенным вариантом полного набора маршрутов виденного в Таблице 2.3. Конечно, эта простота является признаком того, что они более продвинутые — мы прошли долгий путь со времени нашей зависимости от scaffolding в Главе 2, и нам более не нужна бОльшая часть их сложности.
Листинг 11.21. Маршруты для ресурса Microposts.
config/routes.rb
SampleApp::Application.routes.draw do
resources :users
resources :sessions, :only => [:new, :create, :destroy]
resources :microposts, :only => [:create, :destroy]
.
.
.
end
| HTTP запрос | URL | Действие | Цель |
|---|---|---|---|
| POST | /microposts | create | создание нового микросообщения |
| DELETE | /microposts/1 | destroy | удаление микросообщения с id 1 |
Таблица 11.2: RESTful маршруты обеспеченные ресурсом Microposts в Листинге 11.21.
11.3.1 Контроль доступа
Мы начнем нашу разработку ресурса Microposts с контроля доступа в контроллере Microposts. Идея проста: какcreate так и destroy действия должны требовать чтобы пользователи вошли в систему. Код RSpec для тестирования этого представлен в Листинге 11.22,
что потребует создания файла Microposts controller spec. (Мы
протестируем и добавим третью защиту — обеспечение того, что только
пользователь, создавший микросообщение может удалить его — в Разделе 11.3.4.)
Листинг 11.22. Тесты контроля доступа для контроллера Microposts.
spec/controllers/microposts_controller_spec.rb
require 'spec_helper'
describe MicropostsController do
render_views
describe "access control" do
it "should deny access to 'create'" do
post :create
response.should redirect_to(signin_path)
end
it "should deny access to 'destroy'" do
delete :destroy, :id => 1
response.should redirect_to(signin_path)
end
end
end
authenticate метод (Листинг 10.11). В то время authenticate
был необходим только в Users контроллере, но теперь мы обнаружили, что
он нам также необходим и в контроллере Microposts, так что мы переместим
authenticate в Sessions хелпер, как показано в Листинге 11.23.12
Листинг 11.23. Перемещение
authenticate метода в Sessions хелпер. app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
def authenticate
deny_access unless signed_in?
end
def deny_access
store_location
redirect_to signin_path, :notice => "Please sign in to access this page."
end
.
.
.
end
authenticate из контроллера Users.)С кодом в Листинге 11.23,
authenticate метод теперь доступен в контроллере Microposts, что означает, что мы можем ограничить доступ к create и destroy действиям с предфильтром показанным в Листинге 11.24. (Так как мы не генерировали его через командную строку, вам следует создать контроллер Microposts вручную.)
Листинг 11.24. Добавление аутентификации в действия контроллера Microposts.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_filter :authenticate
def create
end
def destroy
end
end
index действие, доступное даже для не вошедших пользователей, мы должны были бы указать защищаемые действия в явном виде:class MicropostsController < ApplicationController
before_filter :authenticate, :only => [:create, :destroy]
def create
end
def destroy
end
end
11.3.2 Создание микросообщений
В Главе 8, мы реализовали регистрацию пользователей, сделав HTML форму которая выдавала HTTP запрос POST вcreate
действие контроллера Users. Реализация создания микросообщения
аналогична; основное отличие заключается в том, что вместо использования
отдельной страницы с адресом /microposts/new, мы (следуя Twitter конвенции) поместим форму на самой Home странице (т.е., root path /), как показано на макете в Рис. 11.9.
Рисунок 11.9: Макет Home страницы с формой для создания микросообщений. (полный размер)
create
действия контроллера Microposts должны обеспечить вход ("фабричного")
пользователя в систему прежде чем пытаться создать сообщение.Держа в уме эту оговорку, тесты для создания микросообщений параллельны аналогичным тестам для создания пользователя из Листинга 8.6 и Листинга 8.14; результаты представлены в Листинге 11.25.
Листинг 11.25. Тесты для
create действия контроллера Microposts. spec/controllers/microposts_controller_spec.rb
require 'spec_helper'
describe MicropostsController do
.
.
.
describe "POST 'create'" do
before(:each) do
@user = test_sign_in(Factory(:user))
end
describe "failure" do
before(:each) do
@attr = { :content => "" }
end
it "should not create a micropost" do
lambda do
post :create, :micropost => @attr
end.should_not change(Micropost, :count)
end
it "should render the home page" do
post :create, :micropost => @attr
response.should render_template('pages/home')
end
end
describe "success" do
before(:each) do
@attr = { :content => "Lorem ipsum" }
end
it "should create a micropost" do
lambda do
post :create, :micropost => @attr
end.should change(Micropost, :count).by(1)
end
it "should redirect to the home page" do
post :create, :micropost => @attr
response.should redirect_to(root_path)
end
it "should have a flash message" do
post :create, :micropost => @attr
flash[:success].should =~ /micropost created/i
end
end
end
end
create микросообщений похоже на его user аналог (Листинг 8.15); принципиальное отличие заключается в использовании пользователь/микросообщение ассоциации для build нового микрособщения, как видно в Листинге 11.26.
Листинг 11.26. Действие
create контроллера Microposts. app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
.
.
.
def create
@micropost = current_user.microposts.build(params[:micropost])
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_path
else
render 'pages/home'
end
end
.
.
.
end
Листинг 11.27. Добавление создания микросообщений к Home странице (/).
app/views/pages/home.html.erb
<% if signed_in? %>
<table class="front" summary="For signed-in users">
<tr>
<td class="main">
<h1 class="micropost">What's up?</h1>
<%= render 'shared/micropost_form' %>
</td>
<td class="sidebar round">
<%= render 'shared/user_info' %>
</td>
</tr>
</table>
<% else %>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>
<%= link_to "Sign up now!", signup_path, :class => "signup_button round" %>
<% end %>
if-else это немного грязно, и его очистка с использованием партиалов остается в качестве упражнения (Раздел 11.5). Однако заполнение необходимых партиалов из Листинг 11.27 не является упражнением; мы заполним партиал формы микросообщений в Листинге 11.28 и сайдбар новой Home страницы в Листинге 11.29.
Листинг 11.28. Партиал формы для создания микросообщений.
app/views/shared/_micropost_form.html.erb
<%= form_for @micropost do |f| %>
<%= render 'shared/error_messages', :object => f.object %>
<div class="field">
<%= f.text_area :content %>
</div>
<div class="actions">
<%= f.submit "Submit" %>
</div>
<% end %>
Листинг 11.29. Партиал для сайдбара с информацией о пользователе.
app/views/shared/_user_info.html.erb
<div class="user_info">
<a href="<%= user_path(current_user) %>">
<%= gravatar_for current_user, :size => 30 %>
<span class="user_name">
<%= current_user.name %>
</span>
<span class="microposts">
<%= pluralize(current_user.microposts.count, "micropost") %>
</span>
</a>
</div>
pluralize.Форма, определенная в Листинге 11.28 является точным аналогом регистрационной формы из Листинга 8.2, что означает, что ей необходима переменная экземпляра
@micropost. Она поставляется в Листинге 11.30 — но только когда пользователь вошел в систему.
Листинг 11.30. Добавление переменной экземпляра micropost в
home действие. app/controllers/pages_controller.rb
class PagesController < ApplicationController
def home
@title = "Home"
@micropost = Micropost.new if signed_in?
end
.
.
.
end
Рисунок 11.10: Home страница (/) с формой для создания нового микросообщения. (полный размер)
Рисунок 11.11: Home страница с ошибками формы. (полный размер)
11.3.3 Предварительная реализация потока сообщений
Комментарий в конце Раздела 11.3.2 намекает на проблему: текущая Home страница не отображает микросообщений. Если вы хотите, вы можете убедиться что форма показанная на Рис. 11.10 работает, введя допустимое микросообщение и затем перейдя на страницу профиля чтобы увидеть сообщение, но это довольно громоздко. Было бы гораздо лучше иметь feed (поток, канал) микросообщений, который включал бы в себя микросообщения пользователя, как показано на макете в Рис. 11.12. (В Главе 12, мы обобщим этот канал (поток) включив микросообщения пользователей за которыми следит текущий пользователь.)
Рисунок 11.12: Макет Home страницы с предварительной реализацией потока сообщений. (полный размер)
feed
методу в модели User. В конце концов, мы протестируем, что поток
сообщений возвращает микросообщения пользователей, за которыми следит
текущий пользователь, но пока мы просто протестируем, что feed метод включает в себя микросообщения текущего пользователя, но исключает сообщения других пользователей. Мы можем выразить эти требования в коде из Листинга 11.31.
Листинг 11.31. Тесты для предварительной реализацией потока сообщений.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
describe "micropost associations" do
before(:each) do
@user = User.create(@attr)
@mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
@mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
end
.
.
.
describe "status feed" do
it "should have a feed" do
@user.should respond_to(:feed)
end
it "should include the user's microposts" do
@user.feed.include?(@mp1).should be_true
@user.feed.include?(@mp2).should be_true
end
it "should not include a different user's microposts" do
mp3 = Factory(:micropost,
:user => Factory(:user, :email => Factory.next(:email)))
@user.feed.include?(mp3).should be_false
end
end
end
end
include?, который просто проверяет что массив включает данный элемент:13$ rails console
>> a = [1, "foo", :bar]
>> a.include?("foo")
=> true
>> a.include?(:bar)
=> true
>> a.include?("baz")
=> false
feed соответствующих микросообщений выбрав все микросообщения с user_id равным id текущего пользователя, чего мы можем достигнуть используя where метод на модели Micropost, как показано в Листинге 11.32.14
Листинг 11.32. Предварительная реализация ленты микросообщений.
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
def feed
# Это предварительное решение. См. полную реализацию в Главе 12.
Micropost.where("user_id = ?", id)
end
.
.
.
end
Micropost.where("user_id = ?", id)
id корректно маскирован прежде чем быть включенным в лежащий в его основе SQL запрос, что позволит избежать серьезной дыры в безопасности называемой SQL инъекция. (id атрибут в данном случае просто целое число, так что в этом случае опасности нет, но постоянное маскирование переменных, вводимых в SQL выражение является хорошей привычкой.)Внимательные читатели могли отметить в этой точке, что код в Листинге 11.32 по сути эквивалентен записи
def feed
microposts
end
Чтобы использовать этот поток сообщений в примере приложения, мы добавим переменную экземпляра
@feed_items для (пагинированного) потока сообщений текущего пользователя как в Листинге 11.33, а затем добавим партиал feed (Листинг 11.34) к Home странице (Листинг 11.36).
Листинг 11.33. Добавление переменной экземпляра feed к
home действию. app/controllers/pages_controller.rb
class PagesController < ApplicationController
def home
@title = "Home"
if signed_in?
@micropost = Micropost.new
@feed_items = current_user.feed.paginate(:page => params[:page])
end
end
.
.
.
end
Листинг 11.34. Партиал _feed.
app/views/shared/_feed.html.erb
<% unless @feed_items.empty? %>
<table class="microposts" summary="User microposts">
<%= render :partial => 'shared/feed_item', :collection => @feed_items %>
</table>
<%= will_paginate @feed_items %>
<% end %>
<%= render :partial => 'shared/feed_item', :collection => @feed_items %>
:collection с элементами потока сообщений, что заставляет render использовать данный партиал для (’feed_item’ в данном случае) рендеринга каждого элемента в коллекции. (Мы опустили :partial параметр в предыдущем рендеринге, написание, например, render ’shared/micropost’, но с :collection параметром этот синтаксис не работает.) Сам партиал элемента потока сообщений представлен в Листинге 11.35; обратите внимание на добавление удаляющей ссылки в партиал feed item, следуя примеру из Листинга 10.38.
Листинг 11.35. Партиал для отдельно взятого feed item.
app/views/shared/_feed_item.html.erb
<tr>
<td class="gravatar">
<%= link_to gravatar_for(feed_item.user), feed_item.user %>
</td>
<td class="micropost">
<span class="user">
<%= link_to feed_item.user.name, feed_item.user %>
</span>
<span class="content"><%= feed_item.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
</span>
</td>
<% if current_user?(feed_item.user) %>
<td>
<%= link_to "delete", feed_item, :method => :delete,
:confirm => "You sure?",
:title => feed_item.content %>
</td>
<% end %>
</tr>
Листинг 11.36. Добавление ленты микросообщений к Home странице.
app/views/pages/home.html.erb
<% if signed_in? %>
<table class="front" summary="For signed-in users">
<tr>
<td class="main">
<h1 class="micropost">What's up?</h1>
<%= render 'shared/micropost_form' %>
<%= render 'shared/feed' %>
</td>
.
.
.
</tr>
</table>
<% else %>
.
.
.
<% end %>
Рисунок 11.13: Home страница (/) с предварительной реализацией потока сообщений. (полный размер)
@feed_items,
таким образом провальная отправка в настоящее время не работает (что вы
можете проверить запустив ваш набор тестов). Самым простым решением
будет полное подавление потока сообщений присвоением ему пустого
массива, как показано в Листинге 11.37.15
Рисунок 11.14: Home страница после создания нового микросообщения. (полный размер)
Листинг 11.37. Добавление (пустой)
@feed_items переменной экземпляра к create действию. app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
.
.
.
def create
@micropost = current_user.microposts.build(params[:micropost])
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_path
else
@feed_items = []
render 'pages/home'
end
end
.
.
.
end
11.3.4 Уничтожение микросообщений
Последний кусок функционала добавляемый к ресурсу Microposts это воможность уничтожения микросообщений. Как и с удалением пользователя (Раздел 10.4.2), мы будем делать это с помощью “delete” ссылок, как показано на макете в Рис. 11.15. В отличие от уничтожения пользователя, где право на удаление имели только администраторы, удаляющие ссылки будут работать только для пользователя, создавшего микросообщения.
Рисунок 11.15: Макет предварительной реализации потока сообщений со ссылками на удаление микросообщений. (полный размер)
Листинг 11.38. Партиал для отображения отдельно взятого микросообщения.
app/views/microposts/_micropost.html.erb
<tr>
<td class="micropost">
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
</td>
<% if current_user?(micropost.user) %>
<td>
<%= link_to "delete", micropost, :method => :delete,
:confirm => "You sure?",
:title => micropost.content %>
</td>
<% end %>
</tr>
micropost.user ассоциация некорректно создается. В результате вызов micropost.user вызывает исключение NoMethodError. Пока этот Rails баг не исправили, в качестве обходного пути вы можете заменить строки<% if current_user?(micropost.user) %>
<% user = micropost.user rescue User.find(micropost.user_id) %>
<% if current_user?(user) %>
micropost.user вызывает исключение, этот код ищет пользователя опираясь на user_id микросообщений.Тест для
destroy действия это просто обобщение аналогичных тестов для уничтожения пользователей (Листинг 10.40), как видно в Листинге 11.39.
Листинг 11.39. Тесты для
destroy действия контроллера Microposts. spec/controllers/microposts_controller_spec.rb
describe MicropostsController do
.
.
.
describe "DELETE 'destroy'" do
describe "for an unauthorized user" do
before(:each) do
@user = Factory(:user)
wrong_user = Factory(:user, :email => Factory.next(:email))
test_sign_in(wrong_user)
@micropost = Factory(:micropost, :user => @user)
end
it "should deny access" do
delete :destroy, :id => @micropost
response.should redirect_to(root_path)
end
end
describe "for an authorized user" do
before(:each) do
@user = test_sign_in(Factory(:user))
@micropost = Factory(:micropost, :user => @user)
end
it "should destroy the micropost" do
lambda do
delete :destroy, :id => @micropost
end.should change(Micropost, :count).by(-1)
end
end
end
end
admin_user, в случае микросообщений мы используем предфильтр authorized_user для проверки того, что текущий пользователь действительно имеет микросообщение с данным id. Код представлен в Листинг 11.40, Листинге 11.40, а результаты уничтожения предпоследнего сообщения представлены на Рис. 11.16.
Листинг 11.40. Действие
destroy контроллера Microposts. app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_filter :authenticate, :only => [:create, :destroy]
before_filter :authorized_user, :only => :destroy
.
.
.
def destroy
@micropost.destroy
redirect_back_or root_path
end
private
def authorized_user
@micropost = current_user.microposts.find_by_id(params[:id])
redirect_to root_path if @micropost.nil?
end
end
where (представлен в Разделе 11.3.3) для поиска микросообщений через ассоциацию:current_user.microposts.find_by_id(params[:id])
find_by_id вместо find так как последнее вызывает исключение в случае если микросообщение не существует вместо того, чтобы вернуть nil. Кстати, если вы хорошо знакомы с исключениями в Ruby, вы также можете написать фильтр authorized_user вроде этого:def authorized_user
@micropost = current_user.microposts.find(params[:id])
rescue
redirect_to root_path
end
authorized_user мы могли бы использовать модель Micropost непосредственно, как здесь:@micropost = Micropost.find_by_id(params[:id])
redirect_to root_path unless current_user?(@micropost.user)
Рисунок 11.16: Home страница пользователя после удаления предпоследнего микросообщения. (полный размер)
11.3.5 Тестирование новой home страницы
Прежде чем оставить создание и уничтожение микросообщений, мы напишем интеграционный тест чтобы проверить что наши формы правильно работают. Как и в случае с пользователями (Раздел 8.4), мы начнем с генерации интеграционного теста микросообщений:$ rails generate integration_test microposts
Листинг 11.41. Интеграционный тест для микросообщений на
home странице. spec/requests/microposts_spec.rb
require 'spec_helper'
describe "Microposts" do
before(:each) do
user = Factory(:user)
visit signin_path
fill_in :email, :with => user.email
fill_in :password, :with => user.password
click_button
end
describe "creation" do
describe "failure" do
it "should not make a new micropost" do
lambda do
visit root_path
fill_in :micropost_content, :with => ""
click_button
response.should render_template('pages/home')
response.should have_selector("div#error_explanation")
end.should_not change(Micropost, :count)
end
end
describe "success" do
it "should make a new micropost" do
content = "Lorem ipsum dolor sit amet"
lambda do
visit root_path
fill_in :micropost_content, :with => content
click_button
response.should have_selector("span.content", :content => content)
end.should change(Micropost, :count).by(1)
end
end
end
end
11.4 Заключение
Добавив ресурс Microposts, мы почти закончили наш пример приложения. Все, что осталось, это добавить социальный слой, позволив пользователям следовать друг за другом. Мы узнаем как моделировать такие отношения пользователей и увидим полноценную реализацию потока сообщений в Главе 12.Если вы используете Git для управления версиями, прежде чем продолжить, убедитесь, что вы зафиксировали и объединили ваши изменения:
$ git add .
$ git commit -m "Added user microposts"
$ git checkout master
$ git merge user-microposts
$ git push heroku
$ heroku rake db:migrate
11.5 Упражнения
Мы рассмотрели достаточно материала и теперь случился комбинаторный взрыв возможных расширений к нашему приложению. Вот лишь некоторые из многих возможностей:- (сложное) Добавить JavaScript отображение к Home странице для обратного отсчета 140 знаков.
- Добавить тесты для отображения количества микросообщений в сайдбаре (включая надлежащие плюрализации).
- (в основном для дизайнеров) Модифицировать список микросообщений чтобы использовать упорядоченный список вместо таблицы. (Примечание: это как Twitter отображает обновление их статуса.) Затем добавить соответствующие CSS, чтобы в результате поток сообщений не выглядел как дерьмо.
- Добавить тесты для пагинации микросообщений.
- Сделать рефакторинг Home страницы чтобы использовать отдельные партиалы для двух ветвей выражения
if-else. - Написать тест чтобы убедиться, что ссылки на удаление не появляются у микросообщений созданных не текущим пользователем.
- Добавить вложенные маршруты так, чтобы по адресу /users/1/microposts показывались все микросообщения пользователя 1. (Вам также необходимо будет добавить в контроллер Microposts действие
indexи соответствующее представление.) - Сейчас очень длинные слова крушат наш шаблон, как это показано на Рис. 11.17. Исправьте эту проблему с помощью хелпера
wrapопределенного в Листинге 11.42. (Обратите внимание на использование методаrawдля предотвращения маскирования Рельсами результирующего HTML, совместно сsanitizeметодом необходимым для предотвращения межсайтового скриптинга.)
Рисунок 11.17: (Порушенный) особенно длинным словом шаблон сайта. (полный размер)
Листинг 11.42. Хелпер для упаковки длинных слов.
app/helpers/microposts_helper.rb
module MicropostsHelper
def wrap(content)
sanitize(raw(content.split.map{ |s| wrap_long_string(s) }.join(' ')))
end
private
def wrap_long_string(text, max_width = 30)
zero_width_space = "​"
regex = /.{1,#{max_width}}/
(text.length < max_width) ? text :
text.scan(regex).join(zero_width_space)
end
end
- Технически, в Главе 9 мы обращались c сессиями как с ресурсом, но они не сохранялись в базе данных как пользователи и микросообщения. ↑
- Атрибут
contentбудетstring, но, как кратко отмечалось в Разделе 2.1.2, для более длинных текстовых полей вам следует использовать тип данныхtext. ↑ - Более подробную информацию о Factory Girl ассоциациях, включая множество доступных опций, см. Factory Girl documentation. ↑
- Вспомните, что
created_atиupdated_atэто “волшебные” столбцы, так что все явные значения инициализации магическим образом перезаписываются. ↑ - В смысле семантической разметки, вероятно, было бы лучше использовать нумерованный список, но в этом случае вертикальное выравнивания текста и изображений было бы гораздо более трудным, чем с таблицами. См. упражнение в Раздел 11.5 если вы настаиваете на трудностях с семантической версией. ↑
- В случае, если вам интересно, метод ассоциаций
countумен, и выполняет подсчет непосредственно в базе данных. В частности, он не вытягивает все микросообщения из базы данных и не вызывает затемlengthна получившийся массив, так как это стало бы страшно неэффективно при увеличении числа микросообщений. Вместо этого, он просит базу данных подсчитать количество микросообщений с даннымuser_id. Кстати, в маловероятном случае при котором count все же будет узким местом в вашем приложении, вы можете сделать его еще быстрее с counter cache. ↑ - Для удобства, Листинг 11.19 на самом деле включает все CSS необходимые для этой главы. ↑
- (т.е., пять с пользовательскими Gravatar-ами, и один с дефолтным Gravatar) ↑
- Следите за своим
log/development.logфайлом, если вам интересно, какой SQL генерирует этот метод. ↑ - Текст lorem ipsum гема Faker случаен, так что контент ваших образцов микросообщений будет отличаться. ↑
- Остальные два ресурса это Users в Разделе 8.1 и Sessions в Разделе 9.1. ↑
- Мы отмечали в Разделе 9.3.2, что вспомогательные методы по умолчанию доступны только в представлениях, но мы сделали вспомогательный метод Sessions доступным также и в контроллерах, добавив
include SessionsHelperв контроллер Application (Листинг 9.11). ↑ - Изучение методов, таких как
include?является одной из причин, по которым, как отмечалось в Разделе 1.1.1, я рекомендую читать книги о чистом Ruby после окончания этого учебника ↑ - См. в Rails Guide Интерфейс запросов Active Record for more on
whereand the like (# предположительно: непередаваемый канадский юмор). ↑ - К сожалению, возвращение пагинированного потока сообщений не работает в данном случае. Реализуйте это и кликните по пагинационной ссылке чтобы увидеть почему. ( Rails Tutorial screencasts раскрывают этот вопрос более подробно.) ↑
Комментариев нет:
Отправить комментарий