Ruby on Rails Tutorial
Изучение Rails на Примерах
Майкл Хартл
Содержание
- Предисловие к русскому изданию
- Глава 1 От нуля к развертыванию
- Глава 2 demo app
- Глава 3 В основном статические страницы
- Глава 4 Rails-приправленный Ruby
- Глава 5 Заполнение шаблона
- Глава 6 Моделирование пользователей
- Chapter 7 Регистрация
- Глава 8 Войти, выйти
- Глава 9 Обновление, демонстрация и удаление пользователей
- Глава 10 Микросообщения пользователей
- Глава 11 Слежение за сообщениями пользователей
Предисловие
Моя компания (CD Baby) была одной из первых громко перешедших на Ruby on Rails, а затем еще громче вернувшейся обратно на PHP (Google расскажет вам об этой драме). Эту книгу, написанную Майклом Хартлом так высоко рекомендовали, что я должен был попробовать её, и Ruby on Rails Tutorial это всё, что я использовал, чтобы вернуться к Rails.
Хотя я уже прошел через много книг по Rails, это одна из немногих, что, наконец, зацепила меня. Было много написано книг типа «Путь Rails» — после которых я чувствовал себя неестественно, но после этой книги я наконец почувствовал себя естественно. Это также единственная книга по Rails, которая соблюдает методику «разработка через тестирование» на всем своем протяжении, этот подход строго рекомендуется специалистами, но он никогда не был так чётко продемонстрирован ранее. Наконец, Git, GitHub и Heroku присутствуют в демо-примерах, автор действительно дает вам почувствовать, что он хотел сделать реальный проект. Учебный код примеров не изолирован.
Линейное повествование — отличный формат. Лично я прошел Rails Tutorial в течении трёх долгих дней, делая все примеры и задачи в конце каждой главы. Делайте всё от начала до конца, не прыгая, и вы получите максимальную пользу.
Наслаждайтесь!
Derek Sivers (sivers.org)
Основатель CD Baby
Благодарности
Ruby On Rails Учебник во многом обязан моей предыдущей книге по Rails, RailsSpace и, следовательно, моему соавтору Aurelius Prochazka. Я хотел бы поблагодарить Aure как за работу, которую он проделал над прошлой книгой, так и за поддержку этой. Я также хотел бы поблагодарить Debra Williams Cauley, редактора обеих книг RailsSpace и Rails Tutorial; до тех пор, пока она не прекратит брать меня на бейсбол, я буду продолжать писать книги для нее.
Я хотел бы поблагодарить огромное количество Рубистов учивших и вдохновлявших меня на протяжении многих лет: David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Pratik Naik, Sarah Mei, Sarah Allen, Wolfram Arnold, Alex Chaffee, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, хороших людей из Pivotal Labs, команду Heroku, thoughtbot ребят, и команду GitHub. Наконец, многих, многих читателей - слишком много чтобы перечислять их здесь - внёсших большое количество предложений по улучшению и сообщивших об ошибках во время написания этой книги, и я с благодарностью признаю их помощь в написании ее настолько хорошей, насколько это было возможно.
Об авторе
Майкл Хартл – автор Ruby on Rails Tutorial, лидирующего введения в веб разработку на Ruby on Rails. Его предыдущий опыт включает в себя написание и разработку RailsSpace - чрезвычайно устаревшего учебника по Rails и разработку Insoshi - некогда популярной, а ныне устаревшей платформы для социальных сетей написанной на Ruby on Rails. В 2011, Майкл получил Ruby Hero Award за его вклад в Ruby сообщество. Он закончил Harvard College, имеет степень Кандидата Физических Наук присвоенную в Caltech и является выпускником предпринимательских курсов Y Combinator.
Копирайт и лицензия
Ruby on Rails Tutorial: Learn Web Development with Rails. Copyright © 2012 by Michael Hartl. Весь исходный код в Ruby on Rails Tutorial доступен под MIT License и Beerware License.
Лицензия MIT
Copyright (c) 2013 Michael Hartl
Данная лицензия разрешает лицам, получившим копию данного программного
обеспечения и сопутствующей документации (в дальнейшем именуемыми
«Программное Обеспечение»), безвозмездно использовать Программное
Обеспечение без ограничений, включая неограниченное право на использование,
копирование, изменение, добавление, публикацию, распространение,
сублицензирование и/или продажу копий Программного Обеспечения, также
как и лицам, которым предоставляется данное Программное Обеспечение,
при соблюдении следующих условий:
Указанное выше уведомление об авторском праве и данные условия должны быть
включены во все копии или значимые части данного Программного Обеспечения.
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО
ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ
ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ
ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ,
ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ
ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
/*
* ----------------------------------------------------------------------------
* "ПИВНАЯ ЛИЦЕНЗИЯ" (Ревизия 42):
* Весь код написан Майклом Хартлом. До тех пор пока вы осознаете это,
* вы можете делать с ним все что захотите. Если мы когда нибудь
* встретимся, и если это того стоило, вы можете купить мне
* пиво в ответ.
* ----------------------------------------------------------------------------
*/
Глава 10 Микросообщения пользователей
В Главе 9 были закончены REST действия для ресурса Users, так что пришло время наконец-то добавить второй ресурс: пользовательские микросообщения.1 Эти короткие сообщения, связанные с конкретным пользователем, впервые были показаны (в зачаточной форме) в Главе 2. В этой главе мы сделаем полноценную версию наброска из Раздела 2.3, сконструировав модель данных Micropost, связав ее с моделью User при помощи has_many
и belongs_to
методов, а затем сделав формы и партиалы, необходимые для манипулирования результатами и их отображения. В Главе 11 мы завершим наш крохотный клон Twitter, добавив понятие слежения за пользователями, с тем чтобы получить поток (feed) их микросообщений.
Если вы используете Git для управления версиями, я предлагаю сделать новую тему ветки, как обычно:
$ git checkout -b user-microposts
10.1 Модель Micropost
Мы начнем Microposts ресурс с создания модели Micropost, которая фиксирует основные характеристики микросообщений. Что мы сделаем основываясь на работе, проделанной в Разделе 2.3; как и модель из того раздела, наша новая модель Micropost будет включать валидации и ассоциации с моделью User. В отличие от той модели, данная Micropost модель будет полностью протестирована, а также будет иметь дефолтное упорядочивание и автоматическую деструкцию в случае уничтожения родительского пользователя.
10.1.1 Базовая модель
Модели Micropost необходимы лишь два атрибута: content
атрибут, содержащий текст микросообщений,2 и user_id
, связывающий микросообщения с конкретным пользователем. Как и в случае с моделью User (Листинг 6.1), мы генерируем ее используя generate model
:
$ rails generate model Micropost content:string user_id:integer
Вполне возможно что помимо всего прочего будет сгенерирована фабрика Micropost которую вам следует удалить так как чуть позже мы создадим ее вручную (Раздел 10.1.4):
$ rm -f spec/factories/microposts.rb
Команда генерации создает миграцию для создания таблицы microposts
в базе данных (Листинг 10.1); сравните ее с аналогичной миграцией для таблицы users
из Листинга 6.2.
user_id
и created_at
.) db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.string :content
t.integer :user_id
t.timestamps
end
add_index :microposts, [:user_id, :created_at]
end
end
Обратите внимание на то, что, поскольку мы ожидаем извлечение всех микросообщений, связанных с данным id пользователя в порядке обратном их созданию, Листинг 10.1 добавляет индексы (Блок 6.2) на столбцы user_id
и created_at
:
add_index :microposts, [:user_id, :created_at]
Включив столбцы user_id
и created_at
в виде массива, мы тем самым сказали Rails о необходимости создания multiple key index, это означает что Active Record использует оба ключа одновременно. Обратите также внимание на строку t.timestamps
, которая (как указано в Разделе 6.1.1) добавляет волшебные столбцы created_at
и updated_at
. Мы будем работать со столбцом created_at
в Разделе 10.1.4 и Разделе 10.2.1.
Мы начнем с минималистичных тестов для модели Micropost, опираясь на аналогичные тесты для модели User (Листинг 6.5). В частности, мы проверим что объект микросообщений отвечает на атрибуты content
и user_id
, как это показано в Листинге 10.2.
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
let(:user) { FactoryGirl.create(:user) }
before do
# Этот код идеоматически некорректен.
@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end
subject { @micropost }
it { should respond_to(:content) }
it { should respond_to(:user_id) }
end
Мы можем получить прохождение этих тестов запустив миграции микросообщений и подготовив тестовую базу данных:
$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
В результате получилась модель Micropost со структурой, показанной на Рис. 10.1.
Вам следует проверить что тесты проходят:
$ bundle exec rspec spec/models/micropost_spec.rb
Даже несмотря на проходящие тесты, вы могли заметить этот код:
let(:user) { FactoryGirl.create(:user) }
before do
# Этот код идеоматически некорректен.
@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end
Коментарий указывает на то что код в before
блоке идеоматически некорректен. Он работает, но это не "Rails way". Посмотрим, сможете ли вы угадать почему. Мы увидим ответ в Разделе 10.1.3.
10.1.2 Первая валидация
Одним из необходимых аспектов модели Micropost является наличие id пользователя для указания на пользователя написавшего микросообщение. Идеоматически корректным способом сделать это является использование ассоциаций Active Record которые мы реализуем в Разделе 10.1.3. Поскольку это подразумевает небольшой рефактринг, в этом разделе мы напишем тест для отлова возможных регрессий.
Как показано в Листинге 10.3, тест валидации для id пользователя просто устанавливает id равным nil
. а затем проверяет что результирующий микропост невалиден. (Сравните с тестами модели User из Листинга 6.8.)
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
let(:user) { FactoryGirl.create(:user) }
before do
# Этот код идеоматически некорректен.
@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end
subject { @micropost }
it { should respond_to(:content) }
it { should respond_to(:user_id) }
it { should be_valid }
describe "when user_id is not present" do
before { @micropost.user_id = nil }
it { should_not be_valid }
end
end
Этот код требует чтобы микросообщение было валидным и тестирует наличие атрибута user_id
. Мы можем получить прохождение этих тестов с помощью простой валидации наличия показанной в Листинге 10.4.
user_id
атрибута микросообщения. app/models/micropost.rb
class Micropost < ActiveRecord::Base
validates :user_id, presence: true
end
10.1.3 Ассоциации Пользователь/Микросообщения
При построении модели данных для веб-приложений, важно иметь возможность создавать связи между отдельными моделями. В данном случае, каждое микросообщение связано с одним пользователем, а каждый пользователь связан с (потенциально) множеством микросообщений — взаимоотношение вкратце рассмотренное в Разделе 2.3.3 и схематически показанное на Рис. 10.2 и Рис. 10.3. В рамках реализации этих связей мы напишем тест для модели Micropost и добавим пару тестов для модели User.
Используя belongs_to
/has_many
ассоциацию определенную в этом разделе, Rails строит методы показанные в Таблице 10.1.
Метод | Назначение |
---|---|
micropost.user | Возвращает объект User связанный с данным микросообщением. |
user.microposts | Возвращает массив микросообщений пользователя. |
user.microposts.create(arg) | Создает микросообщение (user_id = user.id ). |
user.microposts.create!(arg) | Создает микросообщение (бросает исключение в случае неудачи). |
user.microposts.build(arg) | Возвращает новый объект Micropost (user_id = user.id ). |
Обратите внимание в Таблице 10.1, что вместо
Micropost.create
Micropost.create!
Micropost.new
мы имеем
user.microposts.create
user.microposts.create!
user.microposts.build
Этот паттерн является каноническим способом создания микросообщений: через их ассоциацию с пользователем. При создании микросообщения таким способом, его user_id
автоматически устанавливается правильное значение. В частности, мы можем заменить код
let(:user) { FactoryGirl.create(:user) }
before do
# Этот код идеоматически некорректен.
@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end
из Листинга 10.3 на
let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }
После того как мы определили правильные ассоциации, получающаяся в результате переменная @micropost
будет автоматически иметь user_id
эквивалентный связанному с ним пользователю.
Как видно в Таблице 10.1, другим результатом определения ассоциации user/micropost является micropost.user
, который просто возвращает пользователя которому принадлежит данное микросообщение. Мы можем протестировать это с помощью методов it
и its
следующим образом:
it { should respond_to(:user) }
its(:user) { should eq user }
Результирующие тесты модели Micropost показаны в Листинге 10.5.
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }
subject { @micropost }
it { should respond_to(:content) }
it { should respond_to(:user_id) }
it { should respond_to(:user) }
its(:user) { should eq user }
it { should be_valid }
describe "when user_id is not present" do
before { @micropost.user_id = nil }
it { should_not be_valid }
end
end
На стороне модели User ассоциации, мы отложим более детализированные тесты, до Раздела 10.1.4; пока мы просто протестируем наличие атрибута microposts
(Листинг 10.6).
microposts
attribute. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
.
.
.
it { should respond_to(:admin) }
it { should respond_to(:microposts) }
.
.
.
end
После всей этой работы, код для реализации ассоциации до смешного короток: мы можем получить прохождение тестов для Листинга 10.5 и Листинга 10.6 добавив всего две строки: belongs_to :user
(Листинг 10.7) и has_many :microposts
(Листинг 10.8).
пренадлежит
пользователю. app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
end
имеет_много
микросообщений. app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts
.
.
.
end
В этой точке вам следует сравнить написанное в Таблице 10.1 с кодом в Листинге 10.5 и Листинге 10.6 для того чтобы убедиться что вы понимаете основу природы ассоциаций. Вам также следует проверить что тесты проходят:
$ bundle exec rspec spec/models
10.1.4 Улучшение микросообщений
Тесты has_many
ассоциации в Листинге 10.6 мало чего тестируют — они просто проверяют существование атрибута microposts
. В этом разделе мы добавим упорядочивание и зависимость к микросообщениям, а также протестируем что user.microposts
метод действительно возвращает массив микросообщений.
Нам нужно будет построить несколько микросообщений в тесте модели User, что означает, что мы должны сделать фабрику микросообщений в этой точке. Для этого нам нужен способ для создания ассоциации в Factory Girl. К счастью, это легко, как видно в Листинге 10.9.
spec/factories.rb
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "Person #{n}" }
sequence(:email) { |n| "person_#{n}@example.com"}
password "foobar"
password_confirmation "foobar"
factory :admin do
admin true
end
end
factory :micropost do
content "Lorem ipsum"
user
end
end
Здесь мы сообщаем Factory Girl о том что микросообщения связаны с пользователем просто включив пользователя в определение фабрики:
factory :micropost do
content "Lorem ipsum"
user
end
Как мы увидим в следующем разделе, это позволяет нам определить фабричные микросообщения следующим образом:
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
Дефолтное пространство (scope)
По умолчанию, использование user.microposts
для вытягивания пользовательских микросообщений из базы данных не дает никаких гарантий сохранения порядка микросообщений, но мы хотим (следуя конвенции блогов и Twitter), чтобы микросообщения выдавались в обратном хронологическом порядке, т.е. последнее созданное сообщение должно быть первым в списке. Для проверки этого порядка мы сначала создаем пару микросообщений следующим образом:
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
Здесь мы указываем (использование временнЫх хелперов обсуждалось в Блоке 8.1) что второй пост был создан совсем недавно, т.e. 1.hour.ago
(один.час.назад), в то время как первый пост был создан 1.day.ago
(один.день.назад). Обратите внимание, насколько удобна Factory Girl в использовании: мы также можем установить created_at
вручную, чего нам не позволяет делать Active Record. (Вспомните что created_at
и updated_at
являются “волшебными” столбцами, автоматически устанавливающими правильные временные метки создания и обновления, так что любая явная инициализация значений переписывается магическим образом.)
Большинство адаптеров баз данных (в том числе адаптер SQLite) возвращает микросообщения в порядке их id, поэтому мы можем организовать начальные тесты, которые почти наверняка провалятся, используя код в Листинге 10.10. Здесь используется метод let!
(читается как “let bang”) вместо let
; причина его использования заключается в том, что переменные let
являются ленивыми, а это означает что они рождаются только при обращении к ним. Проблема в том, что мы хотим чтобы микросообщения появились незамедлительно, так, чтобы временные метки были в правильном порядке и так, чтобы @user.microposts
не было пустым. Мы достигаем этого с let!
, который принуждает соответствующие переменные появляться незамедлительно.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
describe "micropost associations" do
before { @user.save }
let!(:older_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
end
let!(:newer_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
end
it "should have the right microposts in the right order" do
expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]
end
end
end
Ключевой строкой здесь является
expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]
указывающая, что сообщения должны быть упорядочены таким образом, чтобы новейшее сообщение было первым. Этот тест должен быть провальным, так как по умолчанию сообщения будут упорядочены по id, т.е., [older_micropost, newer_micropost]
. Эти тесты также тестируют базовую корректность самой has_many
ассоциации, проверяя (как указано в Таблице 10.1) что user.microposts
является массивом микросообщений. Метод to_a
, который мы обсуждали ранее в Разделе 4.3.1, конвертирует @user.microposts
из их дефолтного состояния (которым оказывается является “collection proxy” из библиотеке Active Record) в массив подходящий для сравнения с тем что мы создали вручную.
Для того чтобы получить прохождение тестов упорядоченности, мы используем Rails средство default_scope
с аргументом order
, как показано в Листинге 10.11. (Это наш первый пример понятия пространства (scope). Мы узнаем о пространстве в более общем контексте в Главе 11.)
default_scope
. app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order('created_at DESC') }
validates :user_id, presence: true
end
За порядок здесь отвечает ’created_at DESC’
, где DESC
это SQL для “по убыванию”, т.е., в порядке убывания от новых к старым.
В Rails 4.0 все скоупы принимают анонимную функцию которая возвращает критерий нужный для данного скоупа, в основном таким образом что скоуп не должен быть оценен немедленно, но может быть загружен позже по запросу (так называемая ленивая оценка). В данном случае такой функцией является
-> { order('created_at DESC') }
Отличительным синтаксическим признаком таких объектов, называемых Proc (процедура) или lambda, является стрелка ->
. Эти объекты принимают блок (Раздел 4.3.2), а затем оценивают его когда их вызывают методом call
. Мы можем увидеть как это работает в консоли:
>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil
(Procs это довольно продвинутая Ruby-тема, так что не переживайте если не смогли уловить смысла в написанном выше.)
Dependent: destroy
Помимо правильного упорядочивания, есть второе уточнение, которое мы хотели бы добавить в микросообщения. Напомним из Раздела 9.4, что администраторы сайта имеют право уничтожать пользователей. Само собой разумеется, что если пользователь уничтожен, то должны быть уничтожены и его микросообщения. Мы можем протестировать это вначале уничтожив пользователя, а затем проверив, что связанных с ним микросообщений больше нет в базе данных.
Для того чтобы как следует протестировать удаление микросообщений нам вначале необходимо получить микросообщение данного пользователя в переменную, а затем удалить пользователя. Простая реализация выглядит примерно так:
microposts = @user.microposts.to_a
@user.destroy
expect(microposts).not_to be_empty
microposts.each do |micropost|
# Make sure the micropost doesn't appear in the database.
end
Здесь вызов to_a
создает копию микросообщений и мы включили строку
expect(microposts).not_to be_empty
для отлова ошибок которые могут возникнуть при случайном удалении to_a
. Проблема в том, что, без to_a
, удаление пользователя будет удалять сообщения в переменной microposts
и, как результат
microposts.each do |micropost|
# Make sure the micropost doesn't appear in the database.
end
вообще ничего не будет тестировать поскольку переменная microposts
будет пустой.
Мы можем выразить ожидание того что микросообщения не появятся в базе данных следующим образом:
microposts.each do |micropost|
expect(Micropost.where(id: micropost.id)).to be_empty
end
Здесь мы использовали Micropost.where
, который возвращает пустой объект в случае если запись не найдена, в то время как Micropost.find
в аналогичной ситуации вызовет исключение, что немного сложнее тестировать. (На случай если вам любопытно, код
expect do
Micropost.find(micropost)
end.to raise_error(ActiveRecord::RecordNotFound)
проделывает этот трюк в данном случае.)
Собрав все вместе мы получаем код в Листинге 10.12.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
describe "micropost associations" do
before { @user.save }
let!(:older_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
end
let!(:newer_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
end
.
.
.
it "should destroy associated microposts" do
microposts = @user.microposts.to_a
@user.destroy
expect(microposts).not_to be_empty
microposts.each do |micropost|
expect(Micropost.where(id: micropost.id)).to be_empty
end
end
end
end
Код приложения, необходимый для прохождения тестов из Листинга 10.12 короче чем одна строка; в самом деле, это всего лишь опция метода ассоциации has_many
, как показано в Листинге 10.13.
app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
.
.
.
end
Здесь опция dependent: :destroy
в
has_many :microposts, dependent: :destroy
приговаривает связанные микросообщения (т.e., те что принадлежат данному пользователю) быть уничтоженными при уничтожении самого пользователя. Это предотвращает застревание в базе данных бесхозных микросообщений при удалении админами пользователей из системы.
В этом окончательном виде ассоциация пользователь/микросообщения готова к использованию и все тесты должны пройти:
$ bundle exec rspec spec/
10.1.5 Валидации контента
Прежде чем покинуть модель Micropost, мы добавим валидации для атрибута content
(следуя примеру из Раздела 2.3.2). Как и user_id
, атрибут content
должен существовать, а его длина не должна превышать 140 символов, что сделает его настоящим микросообщением. Тесты в основном следуют примерам из тестов валидации модели User в Разделе 6.2, как это показано в Листинге 10.14.
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }
.
.
.
describe "when user_id is not present" do
before { @micropost.user_id = nil }
it { should_not be_valid }
end
describe "with blank content" do
before { @micropost.content = " " }
it { should_not be_valid }
end
describe "with content that is too long" do
before { @micropost.content = "a" * 141 }
it { should_not be_valid }
end
end
Как и в Разделе 6.2, код в Листинге 10.14 использует мультипликацию строк для тестирования валидации длины микросообщения:
$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Код приложения укладывается в одну строку:
validates :content, presence: true, length: { maximum: 140 }
Результирующая модель Micropost показана в Листинге 10.15.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order('created_at DESC') }
validates :content, presence: true, length: { maximum: 140 }
validates :user_id, presence: true
end
10.2 Просмотр микросообщений
Хотя у нас еще нет способа создания микросообщений через веб — он появится лишь в Разделе 10.3.2 — это не остановит нас от их отображения (и тестирования этого отображения). Следуя по стопам Twitter, мы запланируем отображение микросообщений пользователя не на отдельной странице microposts index
, а непосредственно на самой странице user show
, как это показано на Рис. 10.4. Мы начнем с довольно простых ERb шаблонов для добавления отображения микросообщений в профиле пользователя, а затем мы добавим микросообщения в заполнитель образцов данных из Раздела 9.3.2 чтобы у нас было что отображать.
Как и в случае обсуждения машинерии входа в Разделе 8.2.1, Раздел 10.2.1 будет часто отправлять несколько элементов в стек на время, а затем выталкивать их оттуда один за другим. Если вы начнете увязать, будте терпеливы; у Раздела 10.2.2 хорошая развязка.
10.2.1 Дополнение страницы показывающей пользователя
Мы начнем с тестов для отображения микросообщений пользователя, которые мы будем создавать в интеграционных спеках для пользователей. Нашей стратегией будет создание пары фабричных микросообщений связанных с пользователем и последующей проверкой того что страница пользователя содержит контент каждого из микросообщений. Мы также проверим, что, как и на Рис. 10.4, также отображается общее количество микросообщений.
Мы можем создать микросообщения с помощью метода let
, но, как и в Листинге 10.10, мы хотим чтобы ассоциация появилась незамедлительно, чтобы сообщения появились на странице пользователя. Для того чтобы достигнуть этого, мы используем вариант с let!
:
let(:user) { FactoryGirl.create(:user) }
let!(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
let!(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }
before { visit user_path(user) }
С таким определением микросообщений, мы можем протестировать их наличие на странице профиля пользователя с помощью кода в Листинге 10.16.
spec/requests/user_pages_spec.rb
require 'spec_helper'
describe "User pages" do
.
.
.
describe "profile page" do
let(:user) { FactoryGirl.create(:user) }
let!(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
let!(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }
before { visit user_path(user) }
it { should have_content(user.name) }
it { should have_title(user.name) }
describe "microposts" do
it { should have_content(m1.content) }
it { should have_content(m2.content) }
it { should have_content(user.microposts.count) }
end
end
.
.
.
end
Обратите внимание на то, что мы можем использовать метод count
через ассоциацию:
user.microposts.count
Метод ассоциации count
умен, и выполняет подсчет непосредственно в базе данных. В частности, он не вытягивает все микросообщения из базы данных и не вызывает затем length
на получившемся массиве, так как это стало бы страшно неэффективно при увеличении числа микросообщений. Вместо этого, он просит базу данных подсчитать количество микросообщений с данным user_id
. Кстати, в маловероятном случае при котором count все же будет узким местом в вашем приложении, вы можете сделать его еще быстрее с помощью counter cache.
Хотя тесты в Листинге 10.16 не позеленеют до Листинга 10.18, мы начнем работать с кодом приложения вставив список микросообщений в страницу профиля пользователя как это показано в Листинге 10.17.
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
.
.
.
<aside>
.
.
.
</aside>
<div class="span8">
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>
Мы займемся списком микросообщений через мгновение, но есть несколько других вещей, которые необходимо отметить в первую очередь. В Листинге 10.17 мы применили if @user.microposts.any?
(конструкцию, которую мы видели ранее в Листинге 7.24) которая дает нам уверенность в том, что пустой список не будет отображен если у пользователя нет микросообщений.
В Листинге 10.17 также обратите внимание на то, что мы превентивно добавили пагинацию для микросообщений
<%= will_paginate @microposts %>
Если вы сравните это с аналогичной строкой на странице списка пользователей в Листинге 9.33, вы увидите, что прежде мы имели просто
<%= will_paginate %>
Это работало потому что в контексте контроллера Users, will_paginate
предполагает существование переменной экземпляра @users
(которая, как мы видели в Разделе 9.3.3, должна принадлежать к классу ActiveRecord::Relation
). В данном случае, поскольку мы все еще в контроллере Users, но хотим пагинировать микросообщения, а не пользователей, мы явно передаем @microposts
переменную в will_paginate
. Конечно, это означает, что мы должны будем определить такую переменную в user show
действии (Листинг 10.19).
Наконец, отметим, что мы воспользовались этой возможностью, чтобы добавить текущее количество микросообщений:
<h3>Microposts (<%= @user.microposts.count %>)</h3>
Как было отмечено, @user.microposts.count
является аналогом метода User.count
, за тем исключением, что он считает микросообщения принадлежащие данному пользователю через ассоциацию пользователь/микросообщения.
Наконец мы подошли к самому списку микросообщений:
<ol class="microposts">
<%= render @microposts %>
</ol>
Этот код, использующий тег упорядоченного списка ol
, отвечает за генерацию списка микросообщений, но, как вы можете видеть, он перекладывает тяжелую работу на партиал микросообщений. Мы видели в Разделе 9.3.4 что код
<%= render @users %>
автоматически рендерит каждого из пользователей в переменной @users
с помощью партиала _user.html.erb
. Аналогичным образом код
<%= render @microposts %>
делает тоже самое для микросообщений. Это означает, что мы должны определить партиал _micropost.html.erb
(вместе с директорией представлений micropost
), как это показано в Листинге 10.18.
app/views/microposts/_micropost.html.erb
<li>
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
</li>
При этом используется удивительный вспомогательный метод time_ago_in_words
, чей эффект мы увидим в Разделе 10.2.2.
До сих пор, несмотря на определение всех соответствующих ERb шаблонов, тесты в Листинге 10.16 были провальными за неимением переменной @microposts
. Мы можем заставить их пройти с кодом из Листинга 10.19.
@microposts
в действие show
контроллера Users. app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def show
@user = User.find(params[:id])
@microposts = @user.microposts.paginate(page: params[:page])
end
.
.
.
end
Обратим здесь внимание, насколько умен paginate
— он работает даже с ассоциацией микросообщений, залезая в таблицу microposts и вытягивая оттуда нужную страницу микросообщений.
В этой точке мы можем взглянуть на нашу новую страницу профиля пользователя на Рис. 10.5. Это довольно… печально. Конечно, это связано с тем, что в настоящее время у нас нет микросообщений. Пришло время изменить это.
10.2.2 Образцы микросообщений
При всей проделанной работе по созданию шаблонов для микросообщений пользователя в Разделе 10.2.1, конец был довольно разочаровывающим. Мы можем исправить эту печальную ситуацию, добавив микросообщения во вноситель образцов данных из Раздела 9.3.2. Добавление образцов микросообщений для всех пользователей займет довольно много времени, поэтому сначала мы выберем только первые шесть пользователей3 используя :limit
опцию метода User.all
:4
users = User.all(limit: 6)
Затем мы сделаем 50 микросообщений для каждого пользователя (достаточно, для переполнения лимита пагинации, равного 30), сгенерируем образец содержимого для каждого микросообщения, используя удобный метод Lorem.sentence гема Faker.. (Faker::Lorem.sentence
возвращает текст lorem ipsum; как отмечалось в Главе 6, lorem ipsum имеет увлекательную предысторию.) Получившийся в результате новый заполнитель образцов данных показан в Листинге 10.20.
lib/tasks/sample_data.rake
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
.
.
.
users = User.all(limit: 6)
50.times do
content = Faker::Lorem.sentence(5)
users.each { |user| user.microposts.create!(content: content) }
end
end
end
Конечно, для генерации новых образцов данных мы должны запустить Рейк задачу db:populate
:
$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake test:prepare
Теперь мы в состоянии воспользоваться плодами наших трудов в Разделе 10.2.1 отобразив информацию для каждого микросообщеня.5 Предварительные результаты показаны на Рис. 10.6.
Страница показанная на Рис. 10.6 не имеет стилей для микросообщений, так что давайте их добавим (Листинг 10.21) и посмотрим что из этого вышло.6 Рис. 10.7, показывает страницу профиля пользователя для первого (вошедшего) пользователя, тогда как Рис. 10.8 показывает профиль для второго пользователя. Наконец, Рис. 10.9 показывает вторую страницу микросообщений для первого пользователя с пагинационными ссылками внизу экрана. Во всех трех случаях, видно что каждое микросообщение отображается вместе со временем его создания (напр., “Posted 1 minute ago.”); это работа метода time_ago_in_words
из Листинга 10.18. Если вы подождете пару минут и перезагрузите страницу, вы увидите, как текст автоматически обновится в соответствии с новым временем.
app/assets/stylesheets/custom.css.scss
.
.
.
/* microposts */
.microposts {
list-style: none;
margin: 10px 0 0 0;
li {
padding: 10px 0;
border-top: 1px solid #e8e8e8;
}
}
.content {
display: block;
}
.timestamp {
color: $grayLight;
}
.gravatar {
float: left;
margin-right: 10px;
}
aside {
textarea {
height: 100px;
margin-bottom: 5px;
}
}
10.3 Манипулирование микросообщениями
Мы закончили моделирование данных и шаблоны для отображения микросообщений, теперь мы обратим наше внимание на интерфейс для их создания через веб. Результатом будет наш третий пример использования формы HTML для создания ресурса — в данном случае, ресурса Microposts.7 В этом разделе мы также увидим первый намек на поток сообщений — понятие, полной реализацией которого мы займемся в Главе 11. Наконец, как и с пользователями, мы сделаем возможным уничтожение микросообщений через веб.
Существует один разрыв с предыдущими соглашениями который стоит отметить: интерфейс ресурса Microposts будет работать главным образом за счет контроллеров Users и StaticPages, так что нам не понадобятся действия вроде new
или edit
в контроллере Microposts; единственное что нам пригодится это create
и destroy
. Это означает, что маршруты для ресурса Microposts необычайно просты, как это показано в Листинге 10.22. Код в Листинге 10.22 в свою очередь приводит к RESTful маршрутам показанным в Таблице 10.2, которые являются сокращенным вариантом полного набора маршрутов виденного нами в Таблице 2.3. Конечно, эта простота является признаком того, что они более продвинутые — мы прошли долгий путь со времени нашей зависимости от scaffolding в Главе 2 и нам более не нужна бОльшая часть его (scaffolding) сложности.
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 |
10.3.1 Контроль доступа
Мы начнем нашу разработку ресурса Microposts с контроля доступа на уровне контроллера Microposts. Идея проста: как create
так и destroy
действия должны требовать чтобы пользователи вошли в систему. Код RSpec для тестирования этого представлен в Листинге 10.23. (Мы протестируем и добавим третью защиту — обеспечение того, что только пользователь создавший микросообщение может удалить его — в Разделе 10.3.4.)
spec/requests/authentication_pages_spec.rb
require 'spec_helper'
describe "Authentication" do
.
.
.
describe "authorization" do
describe "for non-signed-in users" do
let(:user) { FactoryGirl.create(:user) }
.
.
.
describe "in the Microposts controller" do
describe "submitting to the create action" do
before { post microposts_path }
specify { expect(response).to redirect_to(signin_path) }
end
describe "submitting to the destroy action" do
before { delete micropost_path(FactoryGirl.create(:micropost)) }
specify { expect(response).to redirect_to(signin_path) }
end
end
.
.
.
end
end
end
Прежде чем использовать (пока-не-построенный) веб-интерфейс для микросообщений, код в Листинге 10.23 действует на уровне отдельных действий микросообщений - стратегия которую мы впервые видели в Листинге 9.13. В данном случае, не вошедшие пользователи не могут отправить POST запрос на /microposts (post microposts_path
, который вызывает create
действие) или отправить DELETE запрос на /microposts/1 (delete micropost_path(micropost)
, который вызывает действие destroy
).
Прежде чем начать писать код приложения, необходимый для прохождения тестов из Листинга 10.23 требуется произвести небольшой рефакторинг. Вспомним из Раздела 9.2.1 что мы внедрили требование входа используя предфильтр который назывался signed_in_user
(Листинг 9.12). Тогда этот метод нужен был нам только в контроллере Users, но теперь мы обнаружили, что он также необходим нам и в контроллере Microposts, так что мы переместим его в Sessions хелпер, как это показано в Листинге 10.24.8
signed_in_user
в Sessions хелпер. app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
def current_user?(user)
user == current_user
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
.
.
.
end
Для того чтобы избежать повторяющегося кода, вам сейчас также следует удалить signed_in_user
из контроллера Users.
С кодом в Листинге 10.24, метод signed_in_user
теперь стал доступным в контроллере Microposts, что означает что мы можем ограничить доступ к действиям create
и destroy
с помощью предфильтра показанного в Листинге 10.25. (Поскольку мы не генерировали его в командной строке, вам следует создать файл контроллера Microposts вручную.)
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :signed_in_user
def create
end
def destroy
end
end
Обратите внимание на то, что мы не ограничили действия к которым применяется предфильтр, поскольку он в настоящее время должен применяться ко всем действиям контроллера. Если бы мы добавили, скажем, index
действие, доступное даже для не вошедших пользователей, мы должны были бы указать защищаемые действия в явном виде:
class MicropostsController < ApplicationController
before_action :signed_in_user, only: [:create, :destroy]
def index
end
def create
end
def destroy
end
end
В этой точке тесты должны пройти:
$ bundle exec rspec spec/requests/authentication_pages_spec.rb
10.3.2 Создание микросообщений
В Главе 7 мы реализовали регистрацию пользователей, сделав HTML форму которая выдавала HTTP запрос POST в create
действие контроллера Users. Реализация создания микросообщения аналогична; основное отличие заключается в том, что вместо использования отдельной страницы с адресом /microposts/new, мы (следуя Twitter конвенции) поместим форму на самой Home странице (т.е., root path /), как показано на Рис. 10.10.
Когда мы последний раз видели Home страницу, она выглядела как на Рис. 5.6 — то есть, у нее была большая жирная “Sign up now!” кнопка посередине. Так как форма для создания микросообщения имеет смысл только в контексте конкретного, вошедшего в систему пользователя, одной из целей данного раздела будет предоставление различных версий Home страницы в зависимости от статуса посетителя. Мы осуществим это в Листинге 10.28 ниже, а пока мы можем заняться написанием тестов. Как и в случае с ресурсом Users, мы будем использовать интеграционные тесты:
$ rails generate integration_test micropost_pages
Тесты создания микросообщения очень похожи на тесты для создания пользователя из Листинга 7.16; результат представлен в Листинге 10.26.
spec/requests/micropost_pages_spec.rb
require 'spec_helper'
describe "Micropost pages" do
subject { page }
let(:user) { FactoryGirl.create(:user) }
before { sign_in user }
describe "micropost creation" do
before { visit root_path }
describe "with invalid information" do
it "should not create a micropost" do
expect { click_button "Post" }.not_to change(Micropost, :count)
end
describe "error messages" do
before { click_button "Post" }
it { should have_content('error') }
end
end
describe "with valid information" do
before { fill_in 'micropost_content', with: "Lorem ipsum" }
it "should create a micropost" do
expect { click_button "Post" }.to change(Micropost, :count).by(1)
end
end
end
end
Мы начнем с create
действия для микросообщений, которое очень похоже на свой аналог для контроллера Users (Листинг 7.26); принципиальное отличие заключается в использовании пользователь/микросообщения ассоциации для build
нового микросообщения, как это видно в Листинге 10.27. Обратите внимание на использование строгих параметров через micropost_params
, который открывает для редактирования через веб только контент микросообщения.
create
контроллера Microposts. app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :signed_in_user
def create
@micropost = current_user.microposts.build(micropost_params)
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
render 'static_pages/home'
end
end
def destroy
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
end
Для того чтобы построить форму для создания микросообщений мы воспользуемся кодом из Листинга 10.28, который предоставляет различный HTML в зависимости от того, является ли посетитель вошедшим.
app/views/static_pages/home.html.erb
<% if signed_in? %>
<div class="row">
<aside class="span4">
<section>
<%= render 'shared/user_info' %>
</section>
<section>
<%= render 'shared/micropost_form' %>
</section>
</aside>
</div>
<% else %>
<div class="center hero-unit">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="https://railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</h2>
<%= link_to "Sign up now!", signup_path,
class: "btn btn-large btn-primary" %>
</div>
<%= link_to image_tag("rails.png", alt: "Rails"), 'https://rubyonrails.org/' %>
<% end %>
Наличие большого количества кода в каждой ветке условного оператора if
-else
это немного грязно и его очистка с помощью партиалов остается в качестве упражнения (Раздел 10.5). Однако заполнение необходимых партиалов из Листинга 10.28 не является упражнением; мы заполним сайдбар новой Home страницы в Листинге 10.29, а партиал формы микросообщений в Листинге 10.30.
app/views/shared/_user_info.html.erb
<%= link_to gravatar_for(current_user, size: 52), current_user %>
<h1>
<%= current_user.name %>
</h1>
<span>
<%= link_to "view my profile", current_user %>
</span>
<span>
<%= pluralize(current_user.microposts.count, "micropost") %>
</span>
Как и в Листинге 9.24, код в Листинге 10.29 использует версию gravatar_for
хелпера определенную в Листинге 7.30.
Отметим, что, как и в сайдбаре профиля (Листинг 10.17), информация о пользователе в Листинге 10.29 отображает общее число микросообщений пользователя. Хотя есть небольшое отличие; в сайдбаре профиля, “Microposts” это метка, и отображаемое Microposts 1 имет смысл. Однако в данном случае выражение “1 microposts” является безграмотным, поэтому мы организуем отображение “1 micropost” (но “2 microposts”) используя удобный вспомогателный метод pluralize
.
Затем мы определим форму для создания микросообщений (Листинг 10.30), которая аналогична форме регистрации из Листинга 7.17.
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, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-large btn-primary" %>
<% end %>
Нам нужно сделать два изменения прежде чем форма в Листинге 10.30 заработает. Во-первых, нам нужно определить @micropost
, что (как и раньше) мы сделаем через ассоциацию:
@micropost = current_user.microposts.build
Результат представлен в Листинге 10.31.
home
действие. app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
@micropost = current_user.microposts.build if signed_in?
end
.
.
.
end
У кода в Листинге 10.31 есть одно весомое достоинство: он сломает наши тесты если мы забудем потребовать входа пользователя.
Второе изменение, необходимое для того чтобы заставить работать Листинг 10.30 это переопределение партиала сообщений об ошибках таким образом чтобы
<%= render 'shared/error_messages', object: f.object %>
работало. Вы можете вспомнить из Листинга 7.23 что партиал сообщений об ошибках явно ссылается на @user
переменную, но в данном случае мы имеем вместо нее переменную @micropost
. Мы должны определить партиал сообщений об ошибках который будет работать независимо от вида объекта который будет ему передан. К счастью, переменная формы f
может иметь доступ к связанному объекту через f.object
, так что в
form_for(@user) do |f|
f.object
является @user
, а в
form_for(@micropost) do |f|
f.object
это @micropost
.
Для того, чтобы передать объект в партиал, мы используем хэш со значением равным объекту и ключом равным выбранному имени переменной, именно этого мы и достигаем с помощью этого кода:
<%= render 'shared/error_messages', object: f.object %>
Другими словами, object: f.object
создает переменную с именем object
в партиале error_messages
. Мы можем применить этот объект для построения кастомизированного сообщения об ошибке, как показано в Листинге 10.32.
app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
<div id="error_explanation">
<div class="alert alert-error">
The form contains <%= pluralize(object.errors.count, "error") %>.
</div>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li>* <%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
В этой точке тесты из Листинга 10.26 должны пройти:
$ bundle exec rspec spec/requests/micropost_pages_spec.rb
К сожалению, теперь рухнули интеграционные тесты пользователя - поскольку формы редактирования и регистрации используют старую версию партиала сообщений об ошибках. Для того чтобы их исправить, мы обновим их как показано в Листинге 10.33 и Листинге 10.34. (Примечаение: ваш код будет отличаться если вы реализовали Листинг 9.49 и Листинг 9.50 из упражнений Раздела 9.6. Mutatis mutandis.)
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
.
.
.
<% end %>
</div>
</div>
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
.
.
.
<% end %>
<%= gravatar_for(@user) %>
<a href="http://gravatar.com/emails">change</a>
</div>
</div>
В этой точке все тесты должны проходить:
$ bundle exec rspec spec/
К тому же, весь HTML этого раздела должен рендериться правильно, показывая форму как на Рис. 10.11 и форму с ошибкой как на Рис. 10.12. Приглашаю вас создать свое микросообщение и убедиться, что все работает — но вам, вероятно, все же следует повременить с этим делом до Раздела 10.3.3.
10.3.3 Предварительная реализация потока сообщений
Комментарий в конце Раздела 10.3.2 намекает на проблему: текущая Home страница не отображает микросообщений. Если вы хотите, вы можете убедиться что форма, показанная на Рис. 10.11 работает, введя допустимое микросообщение и затем перейдя на страницу профиля чтобы увидеть сообщение, но это довольно громоздко. Было бы гораздо лучше иметь feed (поток, канал) микросообщений, который включал бы в себя микросообщения пользователя, как показано на Рис. 10.13. (В Главе 11 мы обобщим этот канал (поток), включив микросообщения пользователей за которыми следит текущий пользователь.)
Так как каждый пользователь должен иметь поток сообщений, мы естественным образом пришли к feed
методу в модели User. В конце концов, мы протестируем, что поток сообщений возвращает микросообщения пользователей, за которыми следит текущий пользователь, но пока мы просто протестируем, что feed
метод включает в себя микросообщения текущего пользователя, но исключает сообщения других пользователей. Мы можем выразить эти требования в коде Листинга 10.35.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
it { should respond_to(:microposts) }
it { should respond_to(:feed) }
.
.
.
describe "micropost associations" do
before { @user.save }
let!(:older_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
end
let!(:newer_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
end
.
.
.
describe "status" do
let(:unfollowed_post) do
FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
end
its(:feed) { should include(newer_micropost) }
its(:feed) { should include(older_micropost) }
its(:feed) { should_not include(unfollowed_post) }
end
end
end
Эти тесты вводят (через булеву конвенцию RSpec) метод массива include?
, который просто проверяет что массив включает данный элемент:9
$ rails console
>> a = [1, "foo", :bar]
>> a.include?("foo")
=> true
>> a.include?(:bar)
=> true
>> a.include?("baz")
=> false
Этот пример показывает насколько гибкой является булевая конвенция RSpec; даже несмотря на то, что include
уже является ключевым словом в Ruby (используется для включения модуля, как это можно увидеть в, например, Листинге 8.14), в этом контексте RSpec правильно угадывает что мы хотим протестировать включение в массив.
Мы можем организовать feed
соответствующих микросообщений, выбрав все микросообщения с user_id
равным id текущего пользователя, чего мы можем достигнуть используя where
метод на модели Micropost
, как это показано в Листинге 10.36.10
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
def feed
# Это предварительное решение. См. полную реализацию в "Following users".
Micropost.where("user_id = ?", id)
end
.
.
.
end
Знак вопроса в
Micropost.where("user_id = ?", id)
гарантирует, что id
корректно маскирован прежде чем быть включенным в лежащий в его основе SQL запрос, что позволит избежать серьезной дыры в безопасности называемой SQL инъекция. Атрибут id
в данном случае просто целое число (т.e., self.id
, уникальный ID пользователя), так что в этом случае опасности нет, но постоянное маскирование переменных, вводимых в SQL выражение является хорошей привычкой.
Внимательные читатели могли отметить в этой точке, что код в Листинге 10.36 по сути эквивалентен записи
def feed
microposts
end
Мы использовали код Листинга 10.36 вместо нее так как он генерализует гораздо более естественным образом полноценный поток микросообщений необходимый в Главе 11.
Для того чтобы протестировать отображение потока сообщений, мы вначале должны создать пару сообщений, а затем проверить что на странице присутствует элемент списка (li
) для каждого из них (Листинг 10.37).
spec/requests/static_pages_spec.rb
require 'spec_helper'
describe "Static pages" do
subject { page }
describe "Home page" do
.
.
.
describe "for signed-in users" do
let(:user) { FactoryGirl.create(:user) }
before do
FactoryGirl.create(:micropost, user: user, content: "Lorem ipsum")
FactoryGirl.create(:micropost, user: user, content: "Dolor sit amet")
sign_in user
visit root_path
end
it "should render the user's feed" do
user.feed.each do |item|
expect(page).to have_selector("li##{item.id}", text: item.content)
end
end
end
end
.
.
.
end
Листинг 10.37 предполагает что каждый элемент потока сообщений имеет уникальный CSS id, так что
expect(page).to have_selector("li##{item.id}", text: item.content)
генерирует совпадение для каждого из них. (Обратите внимание что первый #
в li##{item.id}
является синтаксисом Capybara для CSS id, тогда как второй #
является началом Рубишной интерполяции строк #{}
.)
Для того чтобы использовать поток микросообщений в примере приложения, мы добавим переменную экземпляра @feed_items
для (пагинированного) потока сообщений текущего пользователя как в Листинге 10.38, а затем добавим партиал потока сообщений (Листинг 10.39) к Home странице (Листинг 10.41). (Добавление тестов пагинаци остается в качестве упражнения; см. Раздел 10.5.)
home
действию. app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
if signed_in?
@micropost = current_user.microposts.build
@feed_items = current_user.feed.paginate(page: params[:page])
end
end
.
.
.
end
app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
<ol class="microposts">
<%= render partial: 'shared/feed_item', collection: @feed_items %>
</ol>
<%= will_paginate @feed_items %>
<% end %>
Партиал потока сообщений перекладывает рендеринг элемента потока сообщений на партиал элемента потока сообщений с помощью кода
<%= render partial: 'shared/feed_item', collection: @feed_items %>
Здесь мы передаем параметр :collection
с элементами потока сообщений, что заставляет render
использовать данный партиал (’feed_item’
в данном случае) для рендеринга каждого элемента в коллекции. (Мы опустили параметр :partial
в предыдущих рендерингах, используя запись, например, render ’shared/micropost’
, но с :collection
параметром этот синтаксис не работает.) Сам партиал элемента потока сообщений представлен в Листинге 10.40.
app/views/shared/_feed_item.html.erb
<li id="<%= feed_item.id %>">
<%= link_to gravatar_for(feed_item.user), feed_item.user %>
<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>
</li>
Листинг 10.40 также добавляет CSS id для каждого элемента потока сообщений с помощью
<li id="<%= feed_item.id %>">
как того требует тест в Листинге 10.37.
Затем мы можем добавить поток сообщений на Home страницу посредством рендеринга партиала потока сообщений как обычно (Листинг 10.41). В результате поток сообщений отображается на Home странице, как и требовалось (Рис. 10.14).
app/views/static_pages/home.html.erb
<% if signed_in? %>
<div class="row">
.
.
.
<div class="span8">
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
</div>
</div>
<% else %>
.
.
.
<% end %>
На данный момент, создание новых микросообщений работает как надо, что показано на Рис. 10.15. Однако есть одна тонкость: при неудачной отправке микросообщения Home страница ожидает переменную экземпляра @feed_items
, таким образом провальная отправка в настоящее время не работает (что вы можете проверить запустив ваш набор тестов). Самым простым решением будет полное подавление потока сообщений через присвоение ему пустого массива, как это показано в Листинге 10.42.11
@feed_items
переменной экземпляра к create
действию. app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
.
.
.
def create
@micropost = current_user.microposts.build(micropost_params)
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
@feed_items = []
render 'static_pages/home'
end
end
.
.
.
end
В этой точке предварительная реализация потока микросообщений должна работать, а набор тестов должен проходить:
$ bundle exec rspec spec/
10.3.4 Уничтожение микросообщений
Последний кусок функционала, добавляемый к ресурсу Microposts это воможность уничтожения микросообщений. Как и с удалением пользователя (Раздел 9.4.2), мы будем делать это с помощью “delete” ссылок, как показано на Рис. 10.16. В отличие от уничтожения пользователя, где право на удаление имели только администраторы, удаляющие ссылки будут работать только для пользователя, создавшего микросообщения.
Нашим первым шагом является добавление удаляющей ссылки в партиал микросообщения как в Листинге 10.40, и пока мы в нем, мы добавим похожую ссылку к партиалу элемента потока из Листинга 10.40. Результат представлен в Листинге 10.43 и Листинге 10.44. (Эти два случая почти идентичны и устранение этого дублирования остается в качестве упражнения (Раздел 10.5).)
app/views/microposts/_micropost.html.erb
<li>
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" },
title: micropost.content %>
<% end %>
</li>
app/views/shared/_feed_item.html.erb
<li id="<%= feed_item.id %>">
<%= link_to gravatar_for(feed_item.user), feed_item.user %>
<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>
<% if current_user?(feed_item.user) %>
<%= link_to "delete", feed_item, method: :delete,
data: { confirm: "You sure?" },
title: feed_item.content %>
<% end %>
</li>
Тест на удаление микросообщений использует Capybara для клика по ссылке “delete” и ожидает что количество Микросообщений уменьшится на 1 (Листинг 10.45).
destroy
контроллера Microposts. spec/requests/micropost_pages_spec.rb
require 'spec_helper'
describe "Micropost pages" do
.
.
.
describe "micropost destruction" do
before { FactoryGirl.create(:micropost, user: user) }
describe "as correct user" do
before { visit root_path }
it "should delete a micropost" do
expect { click_link "delete" }.to change(Micropost, :count).by(-1)
end
end
end
end
Код приложения также аналогичен коду для удаления пользователей из Листинга 9.46; главное отличие в том, что вместо использования предфильтра admin_user
, в случае микросообщений мы используем предфильтр correct_user
для проверки того, что текущий пользователь действительно имеет микросообщение с данным id. Код представлен в Листинге 10.46, а результаты уничтожения предпоследнего сообщения представлены на Рис. 10.17.
destroy
контроллера Microposts. app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :signed_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
.
.
.
def destroy
@micropost.destroy
redirect_to root_url
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
def correct_user
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
end
В предфильтре correct_user
обратите внимание на то, что мы ищем микросообщения через ассоциацию:
current_user.microposts.find_by(id: params[:id])
Это автоматически обеспечивает поиск лишь микросообщений принадлежащих текущему пользователю. В данном случае мы используем find_by
вместо find
так как последнее вызывает исключение в случае если микросообщение не существует вместо того, чтобы вернуть nil
. Кстати, если вы хорошо знакомы с исключениями в Ruby, вы также можете написать фильтр correct_user
вроде этого:
def correct_user
@micropost = current_user.microposts.find(params[:id])
rescue
redirect_to root_url
end
Мы могли бы реализовать фильтр correct_user
непосредственно через модель Micropost
, например так:
@micropost = Micropost.find_by(id: params[:id])
redirect_to root_url unless current_user?(@micropost.user)
Это было бы эквивалентно коду в Листинге 10.46, но, как объяснял Wolfram Arnold с своем блоге Access Control 101 in Rails and the Citibank Hack, в целях безопасности, хорошей практикой является выполнение поиска только через ассоциацию.
С кодом в этом разделе наша модель Micropost и интерфейс завершены и набор тестов должен пройти:
$ bundle exec rspec spec/
10.4 Заключение
Добавив ресурс Microposts, мы почти закончили наш пример приложения. Все, что осталось, это добавить социальный слой, позволив пользователям следовать друг за другом. Мы узнаем как моделировать такие отношения пользователей и увидим полноценную реализацию потока сообщений в Главе 11.
Прежде чем продолжать, убедитесь что вы закоммитили и объединили ваши изменения если вы используете Git для контроля версий:
$ git add .
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push
Вы также можете отправить приложение на Heroku. Поскольку модель данных изменилась из-за добавления таблицы microposts
, вам также потребуется запустить миграцию продакшен базы данных:
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:populate
10.5 Упражнения
Мы рассмотрели достаточно материала и теперь случился комбинаторный взрыв возможных расширений к нашему приложению. Вот лишь некоторые из многих возможностей:
- Добавить тесты для отображения количества микросообщений в сайдбаре (включая надлежащие плюрализации).
- Добавить тесты для пагинации микросообщений.
- Сделать рефакторинг Home страницы чтобы использовать отдельные партиалы для двух ветвей выражения
if
-else
. - Написать тест чтобы убедиться, что ссылки на удаление не появляются у микросообщений созданных не текущим пользователем.
- Удалить с помощью партиалов дублирование кода в удаляющих ссылках из Листинга 10.43 и Листинга 10.44.
- Сейчас очень длинные слова крушат наш шаблон, как это показано на Рис. 10.18. Исправьте эту проблему с помощью хелпера
wrap
определенного в Листинге 10.47. Обратите внимание на использование методаraw
для предотвращения маскирования Рельсами результирующего HTML, совместно сsanitize
методом необходимым для предотвращения межсайтового скриптинга. Этот код также использует странно выглядящий, но полезный тернарный оператор (Блок 10.1). - (сложное) Добавить JavaScript отображение к Home странице для обратного отсчета 140 знаков.
В мире существует 10 типов людей: Те, кому нравится тернарный оператор, те, кому он не нравится, и те, кто не знает о нем. (Если вам случилось быть в третьей категории, скоро вы ее покинете.)
Когда вы много программируете, вы быстро узнаете, что одним из наиболее распространенных битов управления потоком, является что то вроде этого:
if boolean? do_one_thing else do_something_else end
Ruby, как и многие другие языки (включая C/C++, Perl, PHP, и Java), позволяет заменить это на гораздо более компактное выражение с помощью тернарного оператора (названного таким образом, потому что он состоит из трех частей):
boolean? ? do_one_thing : do_something_else
Вы можете также использовать тернарный оператор в качестве замены для назначения:
if boolean? var = foo else var = bar end
становится
var = boolean? ? foo : bar
Другим распространенным случаем использования является возвращаемое значение функции:
def foo do_stuff boolean? ? "bar" : "baz" end
Поскольку Ruby неявно возвращает значение последнего выражения в функции, здесь метод foo возвращает "bar" или "baz" в зависимости от значения boolean?. Именно эта конструкция представлена в Листинге 10.47.
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
- Технически, в Главе 8 мы обращались c сессиями как с ресурсом, но они не сохранялись в базе данных как пользователи и микросообщения. ↑
- Атрибут
content
будетstring
, но, как кратко отмечалось в Разделе 2.1.2, для более длинных текстовых полей вам следует использовать тип данныхtext
. ↑ - (т.e., пять пользователей с кастомными Gravatars, и один с дефолтным) ↑
- Проследите за своим
log/development.log
файлом если вам интересен SQL который генерирует этот метод. ↑ - Устройство гема Faker таково, что текст lorem ipsum случаен, поэтому контент ваших образцов микросообщений будет отличаться. ↑
- Для удобства, Листинг 10.21 на самом деле содержит весь CSS необходимый для этой главы. ↑
- Другими двумя ресурсами являются Users в Разделе 7.2 и Sessions в Разделе 8.1. ↑
- Мы отмечали в Разделе 8.2.1, что вспомогательные методы по умолчанию доступны только в представлениях, но мы сделали вспомогательный метод Sessions доступным также и в контроллерах, добавив
include SessionsHelper
в контроллер Application (Листинг 8.14). ↑ - Изучение методов, таких как
include?
является одной из причин, по которым, как отмечалось в Разделе 1.1.1, я рекомендую читать книги о чистом Ruby после окончания этого учебника. ↑ - См. Rails Guide Active Record Query Interface для того, чтобы узнать больше о
where
. ↑ - К сожалению, возвращение пагинированного потока сообщений не работает в данном случае. Реализуйте это и кликните по пагинационной ссылке чтобы увидеть почему. ↑