Ruby on Rails Tutorial

Изучение Веб Разработки на Rails

Michael Hartl

Содержание

  1. Предисловие к русскому изданию
  2. Глава 1 От нуля к развертыванию
    1. 1.1 Введение
      1. 1.1.1 Комментарии для разных читателей
      2. 1.1.2 “Масштабирование” Rails
      3. 1.1.3 Соглашения в этой книге
    2. 1.2 За работу
      1. 1.2.1 Среда разработки
        1. Интегрированные Среды Разработки
        2. Текстовые редакторы и командная строка
        3. Браузеры
        4. Примечание об инструментах
      2. 1.2.2 Ruby, RubyGems, Rails и Git
        1. Rails Installer (Windows)
        2. Установка Git
        3. Установка Ruby
        4. Установка RubyGems
        5. Установка Rails
      3. 1.2.3 Первое приложение
      4. 1.2.4 Bundler
      5. 1.2.5 rails server
      6. 1.2.6 Модель-представление-контроллер (MVC)
    3. 1.3 Управление версиями с Git
      1. 1.3.1 Установка и настройка
        1. Первоначальная настройка системы
        2. Первоначальная настройка репозитория
      2. 1.3.2 Добавление и фиксация
      3. 1.3.3 Что хорошего Git делает для вас?
      4. 1.3.4 GitHub
      5. 1.3.5 Ветвление, редактирование, фиксация, объединение
        1. Ветвление
        2. Редактирование
        3. Фиксация
        4. Объединение
        5. Отправка
    4. 1.4 Развертывание
      1. 1.4.1 Установка Heroku
      2. 1.4.2 Развертывание на Heroku, шаг первый
      3. 1.4.3 Развертывание на Heroku, шаг второй
      4. 1.4.4 Команды Heroku
    5. 1.5 Заключение
  3. Глава 2 demo app
    1. 2.1 Планирование приложения
      1. 2.1.1 Моделирование пользователей
      2. 2.1.2 Моделирование микросообщений
    2. 2.2 Ресурс Users
      1. 2.2.1 Обзор пользователя
      2. 2.2.2 MVC в действии
      3. 2.2.3 Недостатки данного ресурса Users
    3. 2.3 Ресурс Microposts
      1. 2.3.1 Микрообзор микросообщений
      2. 2.3.2 Помещение micro в микросообщения
      3. 2.3.3 Пользователь has_many микросообщений
      4. 2.3.4 Иерархия наследования
      5. 2.3.5 Развертывание демонстрационного приложения
    4. 2.4 Заключение
  4. Глава 3 В основном статические страницы
    1. 3.1 Статические страницы
      1. 3.1.1 Истинно статические страницы
      2. 3.1.2 Статические страницы с Rails
    2. 3.2 Наши первые тесты
      1. 3.2.1 Разработка через тестирование
      2. 3.2.2 Добавление страницы
        1. Красный
        2. Зеленый
        3. Refactor (реорганизация)
    3. 3.3 Немного динамические страницы
      1. 3.3.1 Тестирование изменения заголовка
      2. 3.3.2 Прохождение тестов заголовка
      3. 3.3.3 Встроенный Ruby
      4. 3.3.4 Устранение дублирования шаблонами
    4. 3.4 Заключение
    5. 3.5 Упражнения
    6. 3.6 Продвинутые настройки
      1. 3.6.1 Устранение bundle exec
        1. RVM Bundler интеграция
        2. binstubs
      2. 3.6.2 Автоматизированные тесты с Guard
      3. 3.6.3 Ускорение тестов со Spork
        1. Guard и Spork
      4. 3.6.4 Тесты внутри Sublime Text
  5. Глава 4 Rails-приправленный Ruby
    1. 4.1 Причины
    2. 4.2 Строки и методы
      1. 4.2.1 Комментарии
      2. 4.2.2 Строки
        1. Выведение на экран
        2. Строки в одинарных кавычках
      3. 4.2.3 Объекты и передача сообщений
      4. 4.2.4 Определение методов
      5. 4.2.5 Возвращение к title хелперу
    3. 4.3 Другие структуры данных
      1. 4.3.1 Массивы и диапазоны
      2. 4.3.2 Блоки
      3. 4.3.3 Хэши и символы
      4. 4.3.4 Вновь CSS
    4. 4.4 Ruby классы
      1. 4.4.1 Конструкторы
      2. 4.4.2 Наследование классов
      3. 4.4.3 Изменение встроенных классов
      4. 4.4.4 Класс контроллер
      5. 4.4.5 Класс User
    5. 4.5 Заключение
    6. 4.6 Упражнения
  6. Глава 5 Заполнение шаблона
    1. 5.1 Добавление некоторых структур
      1. 5.1.1 Навигация по сайту
      2. 5.1.2 Bootstrap и кастомные CSS
      3. 5.1.3 Частичные шаблоны (partials)
    2. 5.2 Sass и файлопровод (asset pipeline)
      1. 5.2.1 Файлопровод
        1. Директории ассетов
        2. Файлы-манифесты
        3. Препроцессоры
        4. Производительность в продакшен
      2. 5.2.2 Синтаксически обалденные таблицы стилей
        1. Вложение
        2. Переменные
    3. 5.3 Ссылки в шаблоне
      1. 5.3.1 Тестирование маршрутов
      2. 5.3.2 Rails маршруты
      3. 5.3.3 Именованные маршруты
      4. 5.3.4 Приятный RSpec
    4. 5.4 Регистрация пользователей: Первый шаг
      1. 5.4.1 Контроллер Users
      2. 5.4.2 URI для регистрации
    5. 5.5 Заключение
    6. 5.6 Упражнения
  7. Глава 6 Моделирование пользователей
    1. 6.1 Модель User
      1. 6.1.1 Миграции базы данных
      2. 6.1.2 Файл модели
        1. Аннотация модели
        2. Доступные атрибуты
      3. 6.1.3 Создание объектов user
      4. 6.1.4 Поиск объектов user
      5. 6.1.5 Обновление объектов user
    2. 6.2 Валидации User
      1. 6.2.1 Начальные тесты для пользователей
      2. 6.2.2 Валидация наличия
      3. 6.2.3 Валидация длины
      4. 6.2.4 Валидация формата
      5. 6.2.5 Валидация уникальности
        1. Предостережение уникальности
    3. 6.3 Добавление безопасного пароля
      1. 6.3.1 Зашифрованный пароль
      2. 6.3.2 Пароль и подтверждение
      3. 6.3.3 Аутентификация пользователя
      4. 6.3.4 У пользователя есть безопасный пароль
      5. 6.3.5 Создание пользователя
    4. 6.4 Заключение
    5. 6.5 Упражнения
  8. Глава 7 Регистрация
    1. 7.1 Демонстрация пользователей
      1. 7.1.1 Отладка и окружения Rails
      2. 7.1.2 Ресурс Users
      3. 7.1.3 Тестирование страницы показывающей пользователя (с фабриками)
      4. 7.1.4 Изображение Gravatar и боковая панель
    2. 7.2 Форма регистрации
      1. 7.2.1 Тесты для регистрации пользователя
      2. 7.2.2 Применение form_for
      3. 7.2.3 HTML формы
    3. 7.3 Провальная регистрация
      1. 7.3.1 Рабочая форма
      2. 7.3.2 Сообщения об ошибках при регистрации
    4. 7.4 Успешная регистрация
      1. 7.4.1 Завершенная форма регистрации
      2. 7.4.2 Флэш
      3. 7.4.3 Первая регистрация
      4. 7.4.4 Развертывание приложения на сервере с SSL
    5. 7.5 Заключение
    6. 7.6 Упражнения
  9. Глава 8 Войти, выйти
    1. 8.1 Сессии и провальный вход
      1. 8.1.1 Sessions контроллер
      2. 8.1.2 Тестирование входа
      3. 8.1.3 Форма для входа
      4. 8.1.4 Обзор отправки формы
      5. 8.1.5 Рендеринг большинстве с флэш сообщением
    2. 8.2 Успешный вход
      1. 8.2.1 Запомнить меня
      2. 8.2.2 Рабочий метод sign_in
      3. 8.2.3 Текущий пользователь
      4. 8.2.4 Изменение ссылок шаблона
      5. 8.2.5 Вход после регистрации
      6. 8.2.6 Выход
    3. 8.3 Введение в Cucumber (опционально)
      1. 8.3.1 Установка и настройка
      2. 8.3.2 Фичи и шаги
      3. 8.3.3 Контрапункт: кастомные проверки RSpec
    4. 8.4 Заключение
    5. 8.5 Упражнения
  10. Глава 9 Обновление, демонстрация и удаление пользователей
    1. 9.1 Обновление пользователей
      1. 9.1.1 Форма для редактирования
      2. 9.1.2 Провальное редактирование
      3. 9.1.3 Успешное редактирование
    2. 9.2 Авторизация
      1. 9.2.1 Требование входа пользователей
      2. 9.2.2 Требование правильного пользователя
      3. 9.2.3 Дружелюбная переадресация
    3. 9.3 Отображение всех пользователей
      1. 9.3.1 Список пользователей
      2. 9.3.2 Образцы пользователей
      3. 9.3.3 Пагинация
      4. 9.3.4 Частичный рефакторинг
    4. 9.4 Уничтожение пользователей
      1. 9.4.1 Административные пользователи
        1. Возвращение к attr_accessible
      2. 9.4.2 Destroy действие
    5. 9.5 Заключение
    6. 9.6 Упражнения
  11. Глава 10 Микросообщения пользователей
    1. 10.1 Модель Micropost
      1. 10.1.1 Базовая модель
      2. 10.1.2 Доступные атрибуты и первая валидация
      3. 10.1.3 Ассоциации Пользователь/Микросообщения
      4. 10.1.4 Улучшение микросообщений
        1. Дефолтное пространство (scope)
        2. Dependent: destroy
      5. 10.1.5 Валидации контента
    2. 10.2 Просмотр микросообщений
      1. 10.2.1 Дополнение страницы показывающей пользователя
      2. 10.2.2 Образцы микросообщений
    3. 10.3 Манипулирование микросообщениями
      1. 10.3.1 Контроль доступа
      2. 10.3.2 Создание микросообщений
      3. 10.3.3 Предварительная реализация потока сообщений
      4. 10.3.4 Уничтожение микросообщений
    4. 10.4 Заключение
    5. 10.5 Упражнения
  12. Глава 11 Слежение за сообщениями пользователей
    1. 11.1 Модель Relationship
      1. 11.1.1 Проблема с моделью данных (и ее решение)
      2. 11.1.2 Ассоциации пользователь/взаимоотношение
      3. 11.1.3 Валидации
      4. 11.1.4 Читаемые пользователи
      5. 11.1.5 Читатели пользователя
    2. 11.2 Веб-интерфейс для читаемых пользователей
      1. 11.2.1 Образцы данных
      2. 11.2.2 Статистика и форма для слежения за сообщениями пользователя
      3. 11.2.3 Страницы с читаемыми и читателями
      4. 11.2.4 Стандартный способ реализации кнопки "читать" (follow)
      5. 11.2.5 Реализация кнопки "читать" (follow) с Ajax
    3. 11.3 Поток сообщений
      1. 11.3.1 Мотивация и стратегия
      2. 11.3.2 Первая реализация потока сообщений
      3. 11.3.3 Подзапросы
      4. 11.3.4 Новый поток сообщений
    4. 11.4 Заключение
      1. 11.4.1 Расширения к примеру приложения
        1. Реплики
        2. Обмен сообщениями
        3. Уведомления о новых читателях
        4. Напоминание пароля
        5. Подтверждение регистрации
        6. RSS канал
        7. REST API
        8. Поиск
      2. 11.4.2 Руководство по дальнейшим ресурсам
    5. 11.5 Упражнения

Foreword

Моя компания (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
В настоящее время: основатель Thoughts Ltd.

Благодарности

Ruby On Rails Учебник во многом обязан моей предыдущей книге по Rails, RailsSpace и, следовательно, моему соавтору Aurelius Prochazka. Я хотел бы поблагодарить Aure как за работу, которую он проделал над прошлой книгой, так и за поддержку этой. Я также хотел бы поблагодарить Debra Williams Cauley, редактора обеих книг RailsSpace и Rails Tutorial; до тех пор, пока она не прекратит брать меня на бейсбол, я буду продолжать писать книги для нее.

Я хотел бы поблагодарить огромное количество Рубистов Rubyists учивших и вдохновлявших меня на протяжении многих лет: 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) 2012 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

Это производит миграцию для создания таблицы microposts в базе данных (Листинг 10.1); сравните ее с аналогичной миграцией для таблицы users из Листинга 6.2.

Листинг 10.1. Миграция Micropost. (Обратите внимание на индекс на 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.8). В частности, мы проверим что объект микросообщений отвечает на атрибуты content и user_id, как это показано в Листинге 10.2.

Листинг 10.2. Начальный спек Micropost.
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before do
    # This code is wrong!
    @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 db:test:prepare

В результате получилась модель Micropost со структурой, показанной на Рис. 10.1.

micropost_model
Рисунок 10.1: Модель данных Micropost.

Вам следует проверить что тесты проходят:

$ 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 блоке неправильный. Посмотрим, сможете ли вы угадать почему. Мы увидим ответ в Разделе 10.1.3.

10.1.2 Доступные атрибуты и первая валидация

Для того чтобы увидеть почему код в блоке before неправильный, мы начнем с теста валидации для модели Micropost (Листинг 10.3). (Сравните с тестом модели User из Листинга 6.11.)

Листинг 10.3. Тест валидации нового микросообщения.
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before do
    # This code is wrong!
    @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.

Листинг 10.4. Валидация для user_id атрибута микросообщения.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content, :user_id
  validates :user_id, presence: true
end

Теперь мы готовы увидеть почему

@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)

является ошибочным. Проблема в том, что по умолчанию (для Rails 3.2.3) все атрибуты нашей модели Micropost являются доступными. Как обсуждалось в Разделе 6.1.2.2 и Разделе 9.4.1.1, это означает что кто-нибудь может легко изменить любой аспект объекта микросоообщений используя клиент командной строки для выдачи вредоносного запроса. Например, злоумышленник может изменить user_id атрибуты на микросообщениях, тем самым связав микросообщения с неправильным пользователем. Это означае что мы должны удалить :user_id из списка attr_accessible, а после того как мы это сделаем, код приведенный выше перестанет работать. Мы исправим это недоразумение в Разделе 10.1.3.

10.1.3 Ассоциации Пользователь/Микросообщения

При построении модели данных для веб-приложений, важно иметь возможность создавать связи между отдельными моделями. В данном случае, каждое микросообщение связано с одним пользователем, а каждый пользователь связан с (потенциально) множеством микросообщений — взаимоотношение вкратце рассматренное в Разделе 2.3.3 и схематически показанное на Рис. 10.2 и Рис. 10.3. В рамках реализации этих связей, мы напишем тест для модели Micropost, который, в отличие от Листинга 10.2, будет совместим с использованием attr_accessible из Листинга 10.7.

micropost_belongs_to_user
Рисунок 10.2: belongs_to отношение между микросообщением и его пользователем.
user_has_many_microposts
Рисунок 10.3: has_many отношение между пользователем и его микросообщениями.

Используя 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: Резюме методов user/micropost ассоциации.

Обратите внимание в Таблице 10.1, что вместо

Micropost.create
Micropost.create!
Micropost.new

мы имеем

user.microposts.create
user.microposts.create!
user.microposts.build

Этот паттерн является каноническим способом создания микросообщений: через их ассоциацию с пользователем. При создании микросообщения таким способом, его user_id автоматически устанавливается правильное значение, что исправляет проблему отмеченную в Разделе 10.1.2. В частности, мы можем заменить код

let(:user) { FactoryGirl.create(:user) }
before do
  # This code is wrong!
  @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 еквивалентный связанному с ним пользователю.

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

Листинг 10.5. Тест для проверки того, что user_id не является доступным.
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 }
  .
  .
  .
  describe "accessible attributes" do
    it "should not allow access to user_id" do
      expect do
        Micropost.new(user_id: user.id)
      end.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
    end
  end
end

Этот тест проверяет, что вызов Micropost.new с непустым user_id вызывает "mass assignment security error exception". Это поведение является дефолтным для Rails 3.2.3, но в предыдущих версиях это поведение отключено по дефолту, так что вам следует убедиться что ваше приложение сконфигурировано должным образом, как это показано в Листинге 10.6.

Листинг 10.6. Проверка того что Rails вызовет ошибку в случае невалидного массового назначения.
config/application.rb
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    config.active_record.whitelist_attributes = true
    .
    .
    .
  end
end

В случае с моделью Micropost, есть лишь один атрибут который нуждается в редактировании через веб-интерфейс, а именно, content атрибут, поэтому нам нужно удалить :user_id из списка доступных, как это показано в Листинге 10.7.

Листинг 10.7. Открытие доступа к атрибуту contentтолько к атрибуту content).
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content

  validates :user_id, presence: true
end

Как видно в Таблице 10.1, другим результатом определения ассоциации user/micropost является micropost.user, который просто возвращает пользователя которому принадлежит данное микросообщение. Мы можем протестировать это с помощью методов it и its следующим образом:

it { should respond_to(:user) }
its(:user) { should == user }

Результирующие тесты модели Micropost показаны в Листинге 10.8.

Листинг 10.8. Тесты для ассоциации микросообщения/пользователь.
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 == user }

  it { should be_valid }

  describe "accessible attributes" do
    it "should not allow access to user_id" do
      expect do
        Micropost.new(user_id: user.id)
      end.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
    end
  end

  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.9).

Листинг 10.9. Тест на наличие атрибута пользовательских 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(:authenticate) }
  it { should respond_to(:microposts) }
  .
  .
  .
end

После всей этой работы, код для реализации ассоциации до смешного короток: мы можем получить прохождение тестов для Листинга 10.8 и Листинга 10.9 добавив всего две строки: belongs_to :user (Листинг 10.10) и has_many :microposts (Листинг 10.11).

Листинг 10.10. Микросообщение пренадлежит пользователю.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content
  belongs_to :user

  validates :user_id, presence: true
end
Листинг 10.11. Пользователь имеет_много микросообщений.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password
  has_many :microposts
  .
  .
  .
end

В этой точке вам следует сравнить написанное в Таблице 10.1 с кодом в Листинге 10.8 и Листинге 10.9 для того чтобы убедиться что вы понимаете основу природы ассоциаций. Вам также следует проверить что тесты проходят:

$ bundle exec rspec spec/models

10.1.4 Улучшение микросообщений

Тесты has_many ассоциации в Листинге 10.9 мало чего тестируют — они просто проверяют существование атрибута microposts. В этом разделе мы добавим упорядочивание и зависимость к микросообщениям, а также протестируем что user.microposts метод действительно возвращает массив микросообщений.

Нам нужно будет построить несколько микросообщений в тесте модели User, что означает, что мы должны сделать фабрику микросообщений в этой точке. Для этого нам нужен способ для создания ассоциации в Factory Girl. К счастью, это легко, как видно в Листинге 10.12.

Листинг 10.12. Полный файл фабрики, включающий новую фабрику для микросообщений.
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 в использовании: мы можем не только назначать пользователя используя массовое назначение (так как фабрики пренебрегают attr_accessible), мы также можем установить created_at вручную, чего нам не позволяет делать Active Record. (Вспомните что created_at и updated_at являются “волшебными” столбцами, автоматически устанавливающими правильные временные метки создания и обновления, так что любая явная инициализация значений переписывается магическим образом.)

Большинство адаптеров баз данных (в том числе адаптер SQLite) возвращает микросообщения в порядке их id, поэтому мы можем организовать начальные тесты, которые почти наверняка провалятся, используя код в Листинге 10.13. Здесь используется метод let! (читается как “let bang”) вместо let; причина его использования заключается в том, что переменные let являются ленивыми, а это означает что они рождаются только при обращении к ним. Проблема в том, что мы хотим чтобы микросообщения появились незамедлительно, так, чтобы временные метки были в правильном порядке и так, чтобы @user.microposts не было пустым. Мы достигаем этого с let!, который принуждает соответствующие переменные появляться незамедлительно.

Листинг 10.13. Тестирование порядка микросообщений пользователя.
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
      @user.microposts.should == [newer_micropost, older_micropost]
    end
  end
end

Ключевой строкой здесь является

@user.microposts.should == [newer_micropost, older_micropost]

указывающая, что сообщения должны быть упорядочены таким образом, чтобы новейшее сообщение было первым. Этот тест должен быть провальным, так как по умолчанию сообщения будут упорядочены по id, т.е., [older_micropost, newer_micropost]. Эти тесты также тестируют базовую корректность самой has_many ассоциации, проверяя (как указано в Таблице 10.1), что user.microposts является массивом микросообщений.

Для того чтобы получить прохождение тестов упорядоченности, мы используем Rails средство default_scope с параметром :order, как показано в Листинге 10.14. (Это наш первый пример понятия пространства (scope). Мы узнаем о пространстве в более общем контексте в Главе 11.)

Листинг 10.14. Упорядочивание микросообщений с 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

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

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

microposts = @user.microposts
@user.destroy
microposts.each do |micropost|
  # Make sure the micropost doesn't appear in the database.
end

К сожалению это не работает из-за тонкости связанной с массивами в Ruby. Назначение массива в Ruby копирует ссылку на массив, а не сам массив, это означает что изменения в оригинальном массива также будут влиять на копию. Например, предположим что мы создали массив, назначили его второй переменной, а затем сделали реверс первого массива с помощью метода reverse!:

$ rails console
>> a = [1, 2, 3]
=> [1, 2, 3]
>> b = a
=> [1, 2, 3]
>> a.reverse!
=> [3, 2, 1]
>> a
=> [3, 2, 1]
>> b
=> [3, 2, 1]

Удивительно, но здесь b реверсировался также как и a. Это произошло из-за того что и a и b указывают на один и тот же массив. (То же самое происходит и с другими структурами данных в Ruby, такими как строки и хэши.)

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

$ rails console --sandbox
>> @user = User.first
>> microposts = @user.microposts
>> @user.destroy
>> microposts
=> []

(Поскольку мы еще не реализовали удаление связанных с пользователем микросообщений, этот код пока не будет работать, он включен только для демонтстрации принципа.) Здесь мы видим что удаление пользователя оставляет переменную microposts без единого элемента; т.е. это пустой массив [].

Такое поведение означает что мы обязаны быть крайне осторожными при создании дубликатов Ruby-объектов. Для дублирования относительно простых объектов, таких как массивы, мы можем использовать метод dup:

$ rails console
>> a = [1, 2, 3]
=> [1, 2, 3]
>> b = a.dup
=> [1, 2, 3]
>> a.reverse!
=> [3, 2, 1]
>> a
=> [3, 2, 1]
>> b
=> [1, 2, 3]

(Это известно как “мелкая копия”. Создание “глубоких копий” является гораздо более сложной задачей и, фактически, у нее нет общего решения, но если вам понадобится скопировать более сложную структуру, такую как вложенный массив, начните с запроса “ruby deep copy” в поисковике.) Применение метода dup к микросообщениям пользователей дает нам код вроде этого:

microposts = @user.microposts.dup
@user.destroy
microposts.should_not be_empty
microposts.each do |micropost|
  # Make sure the micropost doesn't appear in the database.
end

Здесь мы включили строку

microposts.should_not be_empty

в качестве проверки для того чтобы иметь возможность отловить ошибки связанные с dup, включая его случайное удаление.3 Полная реализация представлена в Листинге 10.15.

Листинг 10.15. Тестирование того, что микросообщения уничтожаются вместе с пользователями.
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.dup
      @user.destroy
      microposts.should_not be_empty
      microposts.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)

проделывает этот трюк.)

Код приложения, необходимый для прохождения тестов из Листинга 10.15 короче чем одна строка; в самом деле, это всего лишь опция метода ассоциации has_many, как показано в Листинге 10.16.

Листинг 10.16. Обеспечение уничтожения микросообщений пользователя вместе с пользователем.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password
  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.17.

Листинг 10.17. Тесты валидаций модели Micropost.
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.17 использует мультипликацию строк для тестирования валидации длины микросообщения:

$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

Код приложения укладывается в одну строку:

validates :content, presence: true, length: { maximum: 140 }

Результирующая модель Micropost показана в Листинге 10.18.

Листинг 10.18. Валидации модели 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

10.2 Просмотр микросообщений

Хотя у нас еще нет способа создания микросообщений через веб — он появится лишь в Разделе 10.3.2 — это не остановит нас от их отображения (и тестирования этого отображения). Следуя по стопам Twitter, мы запланируем отображение микросообщений пользователя не на отдельной странице microposts index, а непосредственно на самой странице user show, как это показано на Рис. 10.4. Мы начнем с довольно простых ERb шаблонов для добавления отображения микросообщений в профиле пользователя, а затем мы добавим микросообщения в заполнитель образцов данных из Раздела 9.3.2 чтобы у нас было что отображать.

user_microposts_mockup_bootstrap
Рисунок 10.4: Набросок страницы профиля пользователя с микросообщениями. (полный размер)

Как и в случае обсуждения машинерии входа в Разделе 8.2.1, Раздел 10.2.1 будет часто отправлять несколько элементов в стек на время, а затем выталкивать их оттуда один за другим. Если вы начнете увязать, будте терпеливы; у Раздела 10.2.2 хорошая развязка.

10.2.1 Дополнение страницы показывающей пользователя

Мы начнем с тестов для отображения микросообщений пользователя, которые мы будем создавать в интеграционных спеках для пользователей. Нашей стратегией будет создание пары фабричных микросообщений связанных с пользователем и последующей проверкой того что страница пользователя содержит контент каждого из микросообщений. Мы также проверим, что, как и на Рис. 10.4, также отображается общее количество микросообщений.

Мы можем создать микросообщения с помощью метода let, но, как и в Листинге 10.13, мы хотим чтобы ассоциация появилась незамедлительно, чтобы сообщения появились на странице пользователя. Для того чтобы достигнуть этого, мы используем вариант с 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.19.

Листинг 10.19. Тесты для отображения микросообщений на странице пользователя.
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_selector('h1',    text: user.name) }
    it { should have_selector('title', text: 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.19 не позеленеют до Листинга 10.21, мы начнем работать с кодом приложения вставив список микросообщений в страницу профиля пользователя как это показано в Листинге 10.20.

Листинг 10.20. Добавление микросообщений с странице показывающей пользователя.
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.20 мы применили if @user.microposts.any? (кончструкцию, которую мы видели ранее в Листинге 7.23) которая дает нам уверенность в том, что пустой список не будет отображен если у пользователя нет микросообщений.

В Листинге 10.20 также обратите внимание на то, что мы превентивно добавили пагинацию для микросообщений

<%= will_paginate @microposts %>

Если вы сравните это с аналогичной строкой на странице списка пользователей в Листинге 9.34, вы увидите, что прежде мы имели просто

<%= will_paginate %>

Это работало, потому что, в контексте контроллера Users, will_paginate предполагает существование переменной экземпляра @users (которая, как мы видели в Разделе 9.3.3, должна принадлежать к классу ActiveRecord::Relation). В данном случае, поскольку мы все еще в контроллере Users, но хотим пагинировать микросообщения, а не пользователей, мы явно передаем @microposts переменную в will_paginate. Конечно, это означает, что мы должны будем определить такую переменную в user show действии (Листинг 10.22).

Наконец, отметим, что мы воспользовались этой возможностью, чтобы добавить текущее количество микросообщений:

<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.21.

Листинг 10.21. Партиал для отображения одного микросообщения.
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 шаблонов, тесты в Listing 10.19 были провальными за неимением переменной @microposts. Мы можем заставить их пройти с кодом из Листинга 10.22.

Листинг 10.22. Добавление переменной экземпляра @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. Это довольно… печально. Конечно, это связано с тем, что в настоящее время у нас нет микросообщений. Пришло время изменить это.

user_profile_no_microposts_bootstrap
Рисунок 10.5: Страница профиля пользователя с кодом для микросообщений, но без микросообщений. (полный размер)

10.2.2 Образцы микросообщений

При всей проделанной работе по созданию шаблонов для микросообщений пользователя в Разделе 10.2.1, конец был довольно разочаровывающим. Мы можем исправить эту печальную ситуацию, добавив микросообщения во вноситель образцов данных из Раздела 9.3.2. Добавление образцов микросообщений для всех пользователей займет довольно много времени, поэтому сначала мы выберем только первые шесть пользователей4 используя :limit опцию метода User.all:5

users = User.all(limit: 6)

Затем мы сделаем 50 микросообщений для каждого пользователя (достаточно, для переполнения лимита пагинации, равного 30), сгенерируем образец содержимого для каждого микросообщения, используя удобный метод Lorem.sentence гема Faker.. (Faker::Lorem.sentence возвращает текст lorem ipsum; как отмечалось в Главе 6, lorem ipsum имеет увлекательную предысторию.) Получившийся в результате новый заполнитель образцов данных показан в Листинге 10.23.

Листинг 10.23. Добавление микросообщений к образцам данных.
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 db:test:prepare

Теперь мы в состоянии воспользоваться плодами наших трудов в Разделе 10.2.1 отобразив информацию для каждого микросообщеня.6 Предварительные результаты показаны на Рис. 10.6.

user_profile_microposts_no_styling_bootstrap
Рисунок 10.6: Профиль пользователя (/users/1) с неотстиленными микросообщениями. (полный размер)

Страница показанная на Рис. 10.6 не имеет стилей для микросообщений, так что давайте их добавим (Листинг 10.24) и посмотрим что из этого вышло.7 Рис. 10.7, показывает страницу профиля пользователя для первого (вошедшего) пользователя, тогда как Рис. 10.8 показывает профиль для второго пользователя. Наконец, Рис. 10.9 показывает вторую страницу микросообщений для первого пользователя с пагинационными ссылками внизу экрана. Во всех трех случаях, видно что каждое микросообщение отображается вместе со временем его создания (напр., “Posted 1 minute ago.”); это работа метода time_ago_in_words из Листинга 10.21. Если вы подождете пару минут и перезагрузите страницу, вы увидите, как текст автоматически обновится в соответствии с новым временем.

Листинг 10.24. CSS для микросообщений (включая весь CSS для этой главы).
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;
  }
}
user_profile_with_microposts_bootstrap
Рисунок 10.7: Профиль пользователя (/users/1) с микросообщениями. (полный размер)
other_profile_with_microposts_bootstrap
Рисунок 10.8: Профиль другого пользователя, тоже с микросообщениями (/users/5). (полный размер)
user_profile_microposts_page_2_rails_3_bootstrap
Рисунок 10.9: Пагинационные ссылки микросообщений (/users/1?page=2). (полный размер)

10.3 Манипулирование микросообщениями

Закончив моделирование данных и шаблоны для отображения микросообщений, сейчас мы обратим наше внимание на интерфейс для их создания через веб. Результатом будет наш третий пример использования формы HTML, для создания ресурса — в данном случае, ресурса Microposts.8 В этом разделе мы также увидим первый намек на поток сообщений — понятие, полной реализацией которого мы займемся в Главе 11. Наконец, как и с пользователями, мы сделаем возможным уничтожение микросообщений через веб.

Существует один разрыв с предыдущими соглашениями, который стоит отметить: интерфейс ресурса Microposts будет работать главным образом за счет контроллеров Users и StaticPages, так что нам не понадобятся действия вроде new или edit в контроллере Microposts; единственное что нам пригодится это create и destroy. Это означает, что маршруты для ресурса Microposts необычайно просты, как показано в Листинге 10.25. Код в Листинге 10.25 в свою очередь приводит к RESTful маршрутам показанным в Таблице 10.2, которые являются сокращенным вариантом полного набора маршрутов виденного нами в Таблице 2.3. Конечно, эта простота является признаком того, что они более продвинутые — мы прошли долгий путь со времени нашей зависимости от scaffolding в Главе 2 и нам более не нужна бОльшая часть его (scaffolding) сложности.

Листинг 10.25. Маршруты для ресурса Microposts.
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users
  resources :sessions,   only: [:new, :create, :destroy]
  resources :microposts, only: [:create, :destroy]
  .
  .
  .
end
HTTP запросURIДействиеНазначение
POST/micropostscreateсоздание нового микросообщения
DELETE/microposts/1destroyудаление микросообщения с id 1
Таблица 10.2: RESTful маршруты обеспеченные ресурсом Microposts в Листинге 10.25.

10.3.1 Контроль доступа

Мы начнем нашу разработку ресурса Microposts с контроля доступа на уровне контроллера Microposts. Идея проста: как create так и destroy действия должны требовать чтобы пользователи вошли в систему. Код RSpec для тестирования этого представлен в Листинге 10.26. (Мы протестируем и добавим третью защиту — обеспечение того, что только пользователь, создавший микросообщение может удалить его — в Разделе 10.3.4.)

Листинг 10.26. Тесты контроля доступа для контроллера Microposts.
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 { response.should redirect_to(signin_path) }
        end

        describe "submitting to the destroy action" do
          before { delete micropost_path(FactoryGirl.create(:micropost)) }
          specify { response.should redirect_to(signin_path) }
        end
      end
      .
      .
      .
    end
  end
end

Прежде чем использовать (пока-не-построенный) веб-интерфейс для микросообщений, код в Листинге 10.26 действует на уровне отдельных действий микросообщений - стратегия которую мы впервые видели в Листинге 9.14. В данном случае, невошедшие пользователи не могут отправить POST запрос на /microposts (post microposts_path, который вызывает create действие) или отправить DELETE запрос на /microposts/1 (delete micropost_path(micropost), который вызывает действие destroy).

Прежде чем начать писать код приложения, необходимый для прохождения тестов из Листинга 10.26 требуется произвести небольшой рефакторинг. Вспомним из Раздела 9.2.1 что мы внедрили требование входа используя предфильтр который назывался signed_in_user (Листинг 9.12). Тогда этот метод нужен был нам только в контроллере Users, но теперь мы обнаружили, что он также необходим нам и в контроллере Microposts, так что мы переместим его в Sessions хелпер, как это показано в Листинге 10.27.9

Листинг 10.27. Перемещение метода 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.27, метод signed_in_user теперь стал доступным в контроллере Microposts, что означает что мы можем ограничить доступ к действиям create и destroy с помощью предфильтра показанного в Листинге 10.28. (Поскольку мы не генерировали его в командной строке, вам следует создать файл контроллера Microposts вручную.)

Листинг 10.28. Добавление аутентификации для действий контроллера Microposts.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_filter :signed_in_user

  def create
  end

  def destroy
  end
end

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

class MicropostsController < ApplicationController
  before_filter :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_page_with_micropost_form_mockup_bootstrap
Рисунок 10.10: Набросок страницы Home с формой для создания микросообщений. (полный размер)

Когда мы последний раз видели Home страницу, она выглядела как на Рис. 5.6 — то есть, у нее была большая жирная “Sign up now!” кнопка посередине. Так как форма для создания микросообщения имеет смысл только в контексте конкретного, вошедшего в систему пользователя, одной из целей данного раздела будет предоставление различных версий Home страницы в зависимости от статуса посетителя. Мы осуществим это в Листинге 10.31 ниже, а пока мы можем заняться написанием тестов. Как и в случае с ресурсом Users, мы будем использовать интеграционные тесты:

$ rails generate integration_test micropost_pages

Тесты создания микросообщения очень похожи на тесты для создания пользователя из Листинга 7.16; результат представлен в Листинге 10.29.

Листинг 10.29. Тесты для создания микросообщений.
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.25); принципиальное отличие заключается в использовании пользователь/микросообщения ассоциации для build нового микросообщения, как это видно в Листинге 10.30.

Листинг 10.30. Действие create контроллера Microposts.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_filter :signed_in_user

  def create
    @micropost = current_user.microposts.build(params[:micropost])
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

  def destroy
  end
end

Для того чтобы построить форму для создания микросообщений мы воспользуемся кодом из Листинга 10.31, который предоставляет различный HTML в зависимости от того, является ли посетитель вошедшим.

Листинг 10.31. Добавление создания микросообщений к Home странице (/).
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.31 не является упражнением; мы заполним сайдбар новой Home страницы в Листинге 10.32, а партиал формы микросообщений в Листинге 10.33.

Листинг 10.32. Партиал для сайдбара с информацией о пользователе.
app/views/shared/_user_info.html.erb
<a href="<%= user_path(current_user) %>">
  <%= gravatar_for current_user, size: 52 %>
</a>
<h1>
  <%= current_user.name %>
</h1>
<span>
  <%= link_to "view my profile", current_user %>
</span>
<span>
  <%= pluralize(current_user.microposts.count, "micropost") %>
</span>

Как и в Листинге 9.25, код в Листинге 10.32 использует версию gravatar_for хелпера определенную в Листинге 7.29.

Отметим, что, как и в сайдбаре профиля (Листинг 10.20), информация о пользователе в Листинге 10.32 отображает общее число микросообщений пользователя. Хотя есть небольшое отличие; в сайдбаре профиля, “Microposts” это метка, и отображаемое Microposts 1 имет смысл. Однако в данном случае выражение “1 microposts” является безграмотным, поэтому мы организуем отображение “1 micropost” (но “2 microposts”) используя удобный вспомогателный метод pluralize.

Затем мы определим форму для создания микросообщений (Листинг 10.33), которая аналогична форме регистрации из Листинга 7.17.

Листинг 10.33. Партиал формы для создания микросообщений.
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.33 заработает. Во-первых, нам нужно определить @micropost, что (как и раньше) мы сделаем через ассоциацию:

@micropost = current_user.microposts.build

Результат представлен в Листинге 10.34.

Листинг 10.34. Добавление переменной экземпляра в home действие.
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    @micropost = current_user.microposts.build if signed_in?
  end
  .
  .
  .
end

У кода в Листинге 10.34 есть одно весомое достоинство: он сломает наши тесты если мы забудем потребовать входа пользователя.

Второе изменение, необходимое для того чтобы заставить работать Листинг 10.33 это переопределение партиала сообщений об ошибках таким образом чтобы

<%= render 'shared/error_messages', object: f.object %>

работало. Вы можете вспомнить из Листинга 7.22 что партиал сообщений об ошибках явно ссылается на @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.35.

Листинг 10.35. Обновление партиала сообщений об ошибках из Листинга 7.23 для работы с другими объектами.
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.29 должны пройти:

$ bundle exec rspec spec/requests/micropost_pages_spec.rb

К сожалению, теперь рухнули интеграционные тесты пользователя - поскольку формы редактирования и регистрации используют старую версию партиала сообщений об ошибках. Для того чтобы их исправить, мы обновим их как показано в Листинге 10.36 и Листинге 10.37. (Примечаение: ваш код будет отличаться если вы реализовали Листинг 9.50 и Листинг 9.51 из упражнений Раздела 9.6. Mutatis mutandis.)

Листинг 10.36. Обновление рендеринга ошибок регистрации пользователей.
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>
Листинг 10.37. Обновление ошибок для редактирования пользователей.
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.

home_with_form_bootstrap
Рисунок 10.11: Страница Home (/) с формой создания нового микросообщения. (полный размер)
home_form_errors_bootstrap
Рисунок 10.12: Home страница с ошибками отправки формы. (полный размер)

10.3.3 Предварительная реализация потока сообщений

Комментарий в конце Раздела 10.3.2 намекает на проблему: текущая Home страница не отображает микросообщений. Если вы хотите, вы можете убедиться что форма, показанная на Рис. 10.11 работает, введя допустимое микросообщение и затем перейдя на страницу профиля чтобы увидеть сообщение, но это довольно громоздко. Было бы гораздо лучше иметь feed (поток, канал) микросообщений, который включал бы в себя микросообщения пользователя, как показано на Рис. 10.13. (В Главе 11 мы обобщим этот канал (поток), включив микросообщения пользователей за которыми следит текущий пользователь.)

proto_feed_mockup_bootstrap
Рисунок 10.13: Набросок страницы Home с предварительной реализацией потока сообщений. (полный размер)

Так как каждый пользователь должен иметь поток сообщений, мы естественным образом пришли к feed методу в модели User. В конце концов, мы протестируем, что поток сообщений возвращает микросообщения пользователей, за которыми следит текущий пользователь, но пока мы просто протестируем, что feed метод включает в себя микросообщения текущего пользователя, но исключает сообщения других пользователей. Мы можем выразить эти требования в коде Листинга 10.38.

Листинг 10.38. Тесты для предварительной реализацией потока сообщений.
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) the метод массива include?, который просто проверяет что массив включает данный элемент:10

$ 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.39.11

Листинг 10.39. Предварительная реализация потока микросообщений.
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 в данном случае просто целое число, так что в этом случае опасности нет, но постоянное маскирование переменных, вводимых в SQL выражение является хорошей привычкой.

Внимательные читатели могли отметить в этой точке, что код в Листинге 10.39 по сути эквивалентен записи

def feed
  microposts
end

Мы использовали код Листинга 10.39 вместо нее так как он генерализует гораздо более естественным образом полноценный поток микросообщений необходимый в Главе 11.

Для того чтобы протестировать отображение потока сообщений, мы вначале должны создать пару сообщений, а затем проверить что на странице присутствует элемент списка (li) для каждого из них (Листинг 10.40).

Листинг 10.40. Тест рендеринга потока сообщений на странице Home.
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|
          page.should have_selector("li##{item.id}", text: item.content)
        end
      end
    end
  end
  .
  .
  .
end

Листинг 10.40 предполагает, что каждый элемент потока сообщений имеет уникальный CSS id, так что

page.should have_selector("li##{item.id}", text: item.content)

генерирует совпадение для каждого из них. (Обратите внимание что первый # в li##{item.id} является синтаксисом Capybara для CSS id, тогда как второй # является началом Рубишной интерполяции строк #{}.)

Для того чтобы использовать поток микросообщений в примере приложения, мы добавим переменную экземпляра @feed_items для (пагинированного) потока сообщений текущего пользователя как в Листинге 10.41, а затем добавим партиал потока сообщений (Листинг 10.42) к Home странице (Листинг 10.44). (Добавление тестов пагинаци остается в качестве упражнения; см. Раздел 10.5.)

Листинг 10.41. Добавление переменной экземпляра потока сообщений к 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
Листинг 10.42. Партиал потока сообщений.
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.43.

Листинг 10.43. Партиал для отдельно взятого элемента потока сообщений.
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.43 также добавляет CSS id для каждого элемента потока сообщений с помощью

<li id="<%= feed_item.id %>">

как того требует тест в Листинге 10.40.

Затем мы можем добавить поток сообщений на Home страницу посредством рендеринга партиала потока сообщений как обычно (Листинг 10.44). В результате поток сообщений отображается на Home странице, как и требовалось (Рис. 10.14).

Листинг 10.44. Добавление потока микросообщений к Home странице.
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 %>
home_with_proto_feed_bootstrap
Рисунок 10.14: Home страница (/) с предварительной реализацией потока сообщений. (полный размер)

На данный момент, создание новых микросообщений работает как надо, что показано на Рис. 10.15. Однако есть одна тонкость: при неудачной отправке микросообщения, Home страница ожидает переменную экземпляра @feed_items, таким образом провальная отправка в настоящее время не работает (что вы можете проверить запустив ваш набор тестов). Самым простым решением будет полное подавление потока сообщений присвоением ему пустого массива, как это показано в Листинге 10.45.12

micropost_created_bootstrap
Рисунок 10.15: Home страница после создания нового микросообщения. (полный размер)
Листинг 10.45. Добавление (пустой) @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_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. В отличие от уничтожения пользователя, где право на удаление имели только администраторы, удаляющие ссылки будут работать только для пользователя, создавшего микросообщения.

micropost_delete_links_mockup_bootstrap
Рисунок 10.16: Набросок предварительной реализации потока сообщений со ссылками на удаление микросообщений. (полный размер)

Нашим первым шагом является добавление удаляющей ссылки в партиал микросообщения как в Листинге 10.43, и пока мы в нем, мы добавим похожую ссылку к партиалу элемента потока из Листинга 10.43. Результат представлен в Листинге 10.46 и Листинге 10.47. (Эти два случая почти идентичны и устранение этого дублирования остается в качестве упражнения (Раздел 10.5).)

Листинг 10.46. Добавление удаляющей ссылки для в партиал микросообщения.
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>
Листинг 10.47. Партиал элемента потока микросообщений с добавленной ссылкой на удаление.
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.48).

Листинг 10.48. Тест для действия 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.48; главное отличие в том, что вместо использования предфильтра admin_user, в случае микросообщений мы используем предфильтр correct_user для проверки того, что текущий пользователь действительно имеет микросообщение с данным id. Код представлен в Листинге 10.49, а результаты уничтожения предпоследнего сообщения представлены на Рис. 10.17.

Листинг 10.49. Действие destroy контроллера Microposts.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_filter :signed_in_user, only: [:create, :destroy]
  before_filter :correct_user,   only: :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    redirect_to root_url
  end

  private

    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_id вместо 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.49, но, как объяснял Wolfram Arnold с своем блоге Access Control 101 in Rails and the Citibank Hack, в целях безопасности, хорошей практикой является выполнение поиска только через ассоциацию.

home_post_delete_bootstrap
Рисунок 10.17: Home страница пользователя после удаления предпоследнего микросообщения. (полный размер)

С кодом в этом разделе наша модель 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 Упражнения

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

  1. Добавить тесты для отображения количества микросообщений в сайдбаре (включая надлежащие плюрализации).
  2. Добавить тесты для пагинации микросообщений.
  3. Сделать рефакторинг Home страницы чтобы использовать отдельные партиалы для двух ветвей выражения if-else.
  4. Написать тест чтобы убедиться, что ссылки на удаление не появляются у микросообщений созданных не текущим пользователем.
  5. Удалить с помощью партиалов дублирование кода в удаляющих ссылках из Листинга 10.46 и Листинга 10.47.
  6. Сейчас очень длинные слова крушат наш шаблон, как это показано на Рис. 10.18. Исправьте эту проблему с помощью хелпера wrap определенного в Листинге 10.50. Обратите внимание на использование метода raw для предотвращения маскирования Рельсами результирующего HTML, совместно с sanitize методом необходимым для предотвращения межсайтового скриптинга. Этот код также использует странно выглядящий, но полезный тернарный оператор (Блок 10.1).
  7. (сложное) Добавить JavaScript отображение к Home странице для обратного отсчета 140 знаков.
long_word_micropost_bootstrap
Рисунок 10.18: (Порушенный) особенно длинным словом шаблон сайта. (полный размер)
Листинг 10.50. Хелпер для упаковки длинных слов.
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 = "&#8203;"
      regex = /.{1,#{max_width}}/
      (text.length < max_width) ? text :
                                  text.scan(regex).join(zero_width_space)
    end
end
  1. Технически, в Главе 8 мы обращались c сессиями как с ресурсом, но они не сохранялись в базе данных как пользователи и микросообщения. 
  2. Атрибут content будет string, но, как кратко отмечалось в Разделе 2.1.2, для более длинных текстовых полей вам следует использовать тип данных text
  3. Изначально я забыл дублировать микросообщения как следует и, фактически, предыдущие версии учебника содержали ошибку в тестах. Наличие проверки могло бы помочь ее отловить. Спасибо внимательному читателю Jacob Turino за обнаружение и указание на ошибку. 
  4. (т.e., пять пользователей с кастомными Gravatars, и один с дефолтным) 
  5. Проследите за своим log/development.log файлом если вам интересен SQL который генерирует этот метод. 
  6. Устройство гема Faker таково, что текст lorem ipsum случаен, поэтому контент ваших образцов микросообщений будет отличаться. 
  7. Для удобства, Листинг 10.24 на самом деле содержит весь CSS необходимый для этой главы. 
  8. Другими двумя ресурсами являются Users в Разделе 7.2 и Sessions в Разделе 8.1
  9. Мы отмечали в Разделе 8.2.1, что вспомогательные методы по умолчанию доступны только в представлениях, но мы сделали вспомогательный метод Sessions доступным также и в контроллерах, добавив include SessionsHelper в контроллер Application (Листинг 8.14). 
  10. Изучение методов, таких как include? является одной из причин, по которым, как отмечалось в Разделе 1.1.1, я рекомендую читать книги о чистом Ruby после окончания этого учебника. 
  11. См. Rails Guide Active Record Query Interface для того, чтобы узнать больше о where
  12. К сожалению, возвращение пагинированного потока сообщений не работает в данном случае. Реализуйте это и кликните по пагинационной ссылке чтобы увидеть почему.