Ruby on Rails Tutorial

Изучение Rails на Примерах

Майкл Хартл

Содержание

  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 Статические страницы
    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 URL для регистрации
    5. 5.5 Заключение
    6. 5.6 Упражнения
  7. Глава 6 Моделирование пользователей
    1. 6.1 Модель User
      1. 6.1.1 Миграции базы данных
      2. 6.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. Chapter 7 Регистрация
    1. 7.1 Демонстрация пользователей
      1. 7.1.1 Отладка и окружения Rails
      2. 7.1.2 A Ресурс 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 Строгие параметры
      3. 7.3.3 Сообщения об ошибках при регистрации
    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. Возвращение к строгим параметрам
      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 Упражнения

Предисловие

Моя компания (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):
 * Весь код написан Майклом Хартлом. До тех пор пока вы осознаете это,
 * вы можете делать с ним все что захотите. Если мы когда нибудь
 * встретимся, и если это того стоило, вы можете купить мне
 * пиво в ответ.
 * ----------------------------------------------------------------------------
 */

Глава 11 Слежение за сообщениями пользователей

В этой главе мы завершим ядро примера приложения, добавив социальный слой, что позволит пользователям читать (и не читать) сообщения других пользователей 17, в результате чего на главной странице каждого пользователя будет отображаться список микросообщений тех пользователей, сообщения которых он читает. Мы также сделаем представления для отображения читающих и читаемых пользователей. Мы узнаем, как смоделировать взаимоотношения между пользователями в Разделе 11.1, а затем сделаем веб-интерфейс в Разделе 11.2 (включая введение в Ajax). Наконец, мы закончим, разработав полнофункциональный поток сообщений в Разделе 11.3.

Эта последняя глава содержит несколько из наиболее сложных материалов учебника, в том числе, сложные модели данных и несколько Ruby / SQL хитростей для создания потока сообщений. С помощью этих примеров вы увидите как Rails может обрабатывать даже весьма сложные модели данных, что должно вам пригодиться, так как вы двигаетесь к разработке собственных приложений с их собственными требованиями. Чтобы помочь с переходом от учебника к самостоятельной разработке, Раздел 11.4 содержит рекомендуемые расширения к ядру примера приложения, а также ссылки на более продвинутые ресурсы.

Как обычно, Git пользователи должны создать новую тему ветки:

$ git checkout -b following-users

Так как материал этой главы особенно сложен, прежде чем писать код, мы улучим момент и сделаем небольшой обзор интерфейса. Как и в предыдущих главах, на этом раннем этапе мы будем представлять страницы используя наброски.1 Полная последовательность страниц работает следующим образом: пользователь, (John Calvin) начинает на странице своего профиля (Рис. 11.1) и переходит на страницу со списком пользователей (Рис. 11.2) для того, чтобы выбрать пользователя, сообщения которого он будет читать. Calvin переходит на страницу профиля выбранного пользователя, Thomas-а Hobbes-а (Рис. 11.3), кликает по кнопке “Follow”, чтобы читать сообщения этого пользователя. Это изменяет кнопку “Follow” на “Unfollow”, и увеличивает количество “followers” товарища Hobbes-а на единицу (Рис. 11.4). Вернувшись на свою главную страницу, Calvin теперь видит увеличившееся количество “following” и обнаруживает микросообщения Hobbes-а в своем потоке сообщений (Рис. 11.5). Остальная часть этой главы посвящена реализации этой последовательности.

page_flow_profile_mockup_bootstrap
Рисунок 11.1: Профиль текущего пользователя. (полный размер)
page_flow_user_index_mockup_bootstrap
Рисунок 11.2: Поиск пользователя для чтения его сообщений. (полный размер)
page_flow_other_profile_follow_button_mockup_bootstrap
Рисунок 11.3: Профиль другого пользователя с “Follow” кнопкой. (полный размер)
page_flow_other_profile_unfollow_button_mockup_bootstrap
Рисунок 11.4: Профиля с “Unfollow” кнопкой и увеличившимся количеством читателей. (полный размер)
page_flow_home_page_feed_mockup_bootstrap
Рисунок 11.5: Главная страница с лентой сообщений и увеличившимся количеством читаемых пользователей. (полный размер)

11.1 Модель Relationship

Наш первый шаг в реализации слежения за сообщениями пользователей, заключается в построении модели данных, которая не так проста, как кажется. Naïvely, кажется, что has_many отношение должно сработать: пользователь has_many (имеет_много) читаемых и has_many (имеет_много) читателей. Как мы увидим, в этом подходе есть проблема, и мы узнаем как ее исправить используя has_many through. Вполне вероятно, что многие идеи этого раздела окажутся непонятыми с первого раза, и может потребоваться некоторое время для осознания довольно сложной модели данных. Если вы обнаружите что запутались, попробуйте пройти главу до конца, а затем прочитать этот раздел еще раз, чтобы прояснить для себя некоторые вещи.

11.1.1 Проблема с моделью данных (и ее решение)

В качестве первого шага на пути построения модели данных для слежения за сообщениями пользователей, давайте рассмотрим следующий типичный случай. Возьмем, в качестве примера, пользователя, который следит за сообщениями второго пользователя: мы могли бы сказать, что, например, Кальвин читает сообщения Гоббса, и Гоббс читается Кальвином, таким образом, Кальвин является читателем, а Гоббс является читаемым. При использовании дефолтной Rails’ плюрализации, множество таких читаемых пользователей называлось бы followers, и user.followers был бы массивом таких пользователей. К сожалению, реверс не сработает: по умолчанию, множество всех читаемых пользователей называлось бы followeds, что является безграмотной неуклюжестью. Мы могли бы назвать их following, но это тоже сомнительная идея: в нормальном английском, “following” это множество людей, следящих за вами, т.e., ваши последователи — с точностью до наоборот от предполагаемого значения. Хотя мы будем использовать “following” в качестве метки, как в “50 following, 75 followers”, мы будем использовать “followed users” для самих пользователей, с соответствующим массивом user.followed_users.2

Это предполагает моделирование читаемых пользователей как на Рис. 11.6, с followed_users таблицей и has_many ассоциацией. Поскольку user.followed_users должно быть массивом пользователей, каждая строка таблицы followed_users должна быть пользователем, идентифицируемым с помощью followed_id, совместно с follower_id для установления ассоциации.3 Кроме того, так как каждая строка является пользователем, мы должны были бы включить другие атрибуты пользователя, включая имя, пароль и т.д.

naive_user_has_many_followed_users
Рисунок 11.6: Наивная реализация слежения за сообщениями пользователя.

Проблема модели данных из Рис. 11.6 в том, что она ужасно избыточна: каждая строка содержит не только id каждого читаемого пользователя, но и всю остальную информацию, уже содержащуюся в таблице users. Еще хуже то, что для моделирования читателей пользователя нам потребуется отдельная followers таблица. Наконец, эта модель данных кошмарно неудобна в эксплуатации, так как каждый раз при изменении пользователем (скажем) своего имени, нам пришлось бы обновлять запись пользователя не только в users таблице, но также каждую строку, содержащую этого пользователя в обоих followed_users и followers таблицах.

Проблема здесь в том, что нам не хватает лежащей в основе абстракции. Один из способов найти правильную абстракцию, это рассмотреть, как мы могли бы реализовать чтение сообщений пользователя в веб-приложении. Вспомним из Раздела 7.1.2, что REST архитектура включает в себя ресурсы которые создаются и уничтожаются. Это приводит нас к двум вопросам: Что создается, когда пользователь начинает читать сообщения другого пользователя? Что уничтожается, когда пользователь прекращает следить за сообщениями другого пользователя?

Поразмыслив, мы видим, что в этих случаях приложение должно создать либо разрушить взаимоотношение между двумя пользователями. Затем пользователь has_many :relationships (имеет_много :взаимоотношений), и имеет много followed_users (или followers) через эти взаимоотношения. Действительно, Рис. 11.6 уже содержит бОльшую часть реализации: поскольку каждый читаемый пользователь уникально идентифицирован посредством followed_id, мы можем преобразовать followed_users в таблицу relationships, опустив информацию о пользователе, и использовав followed_id для получения читаемых пользователей из users таблицы. Кроме того, приняв во внимание обратные взаимоотношения, мы могли бы использовать follower_id столбец для извлечения массива читателей пользователя.

Для того, чтобы создать массив пользователей followed_users, мы могли бы вытянуть массив атрибутов followed_id, а затем найти пользователя для каждого из них. Однако, как и следовало ожидать, в Rails есть более удобный способ для этой процедуры; соответствующая техника известна как has_many through. Как мы увидим в Разделе 11.1.4, Rails позволяет нам сказать, что пользователь следит за сообщениями многих пользователей через таблицу взаимоотношений, используя краткий код

has_many :followed_users, through: :relationships, source: :followed

Этот код автоматически заполняет user.followed_users массивом читаемых пользователей. Схема модели данных представлена на Рис. 11.7.

user_has_many_followed_users
Рисунок 11.7: Модель слежения пользователя за сообщениями через взаимоотношения.

Чтобы начать работу над реализацией, мы сначала генерируем модель Relationship следующим образом:

$ rails generate model Relationship follower_id:integer followed_id:integer

Возможно при этом была сгенерирована фабрика Relationship которую вам следует удалить:

$ rm -f spec/factories/relationship.rb

Так как мы будем искать взаимоотношения по follower_id и по followed_id, мы должны добавить индекс на каждой колонке для повышения эффективности поиска, как показано в Листинге 11.1.

Листинг 11.1. Добавление индексов для relationships таблицы.
db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

Листинг 11.1 включает также составной индекс, который обеспечивает уникальность пар (follower_id, followed_id), так что пользователь не может следить за сообщениями другого пользователя более одного раза:

add_index :relationships, [:follower_id, :followed_id], unique: true

(Сравните с индексом уникальности email из Листинга 6.19.) Как мы увидим в Разделе 11.1.4, наш пользовательский интерфейс не позволит этому случиться, но добавление индекса уникальности позволит избежать ошибки в случае, если пользователь попытается дублировать взаимотношения любым другим способом (используя, например, инструмент командной строки, такой как curl). Мы могли бы также добавить валидацию уникальности к модели Relationship, но, так как дублирование взаимоотношений является ошибкой всегда, для наших целей вполне достаточно индекса уникальности.

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

$ bundle exec rake db:migrate
$ bundle exec rake test:prepare

Результирующая модель данных Relationship показана на Рис. 11.8.

relationship_model
Рисунок 11.8: Модель данных Relationship.

11.1.2 Ассоциации пользователь/взаимоотношение

Прежде чем приступить к реализации читателей и читаемых, нам вначале необходимо установить ассоциацию между пользователями и взаимоотношениями. Пользователь has_many (имеет_много) взаимоотношений, и, так как взаимоотношения включают двух пользователей — взаимоотношение belongs_to (принадлежит_к) читающим и читаемым пользователям.

Как и с микросообщениями в Разделе 10.1.3, мы будем создавать новые взаимоотношения используя ассоциацию, с помощью такого кода

user.relationships.build(followed_id: ...)

Мы начнем с базовых тестов валидации показанных в Листинге 11.2.

Листинг 11.2. Тестирование создания Relationship и атрибутов.
spec/models/relationship_spec.rb
require 'spec_helper'

describe Relationship do

  let(:follower) { FactoryGirl.create(:user) }
  let(:followed) { FactoryGirl.create(:user) }
  let(:relationship) { follower.relationships.build(followed_id: followed.id) }

  subject { relationship }

  it { should be_valid }
end

Обратите внимание, что, в отличие от тестов для моделей User и Micropost, которые использовали @user и @micropost, соответственно, Листинг 11.2 использует let вместо переменных экземпляра. Отличия между ними редко имеют значение,4 но я считаю let более чистым решением, нежели переменные экземпляра. Ранее мы применяли переменные экземпляра из-за того что нам важно было как можно раньше ввести переменные экземпляра, а также потому что let является немного более продвинутой техникой.

Мы также должны протестировать атрибут relationships модели User как это показано в Листинге 11.3.

Листинг 11.3. Тестирование атрибута user.relationships.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:feed) }
  it { should respond_to(:relationships) }
  .
  .
  .
end

В этой точке вы, возможно, ожидаете код приложения как в Разделе 10.1.3 - и он действительно похож, но есть одно важное отличие: в случае с моделью Micropost мы могли сказать

class Micropost < ActiveRecord::Base
  belongs_to :user
  .
  .
  .
end

и

class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end

поскольку у таблицы microposts есть атрибут user_id для идентификации пользователя (Раздел 10.1.1). Id используемый таким способом для связи двух таблиц базы данных, известен как внешний ключ, и когда внешним ключом для объекта модели User является user_id, Rails может вывести ассоциацию автоматически: по умолчанию, Rails ожидает внешний ключ в форме <class>_id, где <class> является строчной версией имени класса.5 В данном случае, несмотря на то, что мы по прежнему имеем дело с пользователями, они теперь отождествляются с внешним ключом follower_id, поэтому мы должны сообщить об этом Rails, как показано в Листинге 11.4.6

Листинг 11.4. Реализация has_many ассоциации пользователь/взаимоотношение.
app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  .
  .
  .
end

(Поскольку уничтожение пользователя должно также уничтожить его взаимоотношения мы пошли еще дальше и добавили dependent: :destroy к ассоциации; написание теста на это останется в качестве упражнения (Section 11.5).)

Как и у модели Micropost, у Relationship модели есть belongs_to взаимоотношения с пользователями; в данном случае, объект взаимоотношение принадлежит к обоим follower и followed пользователям, что мы и тестируем в Листинге 11.5.

Листинг 11.5. Тестирование belongs_to ассоциации пользователь/взаимоотношения.
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "follower methods" do
    it { should respond_to(:follower) }
    it { should respond_to(:followed) }
    its(:follower) { should eq follower }
    its(:followed) { should eq followed }
  end
end

Чтобы написать код приложения, мы определяем belongs_to взаимоотношения как обычно. Rails выводит названия внешних ключей из соответствующих символов (т.e., follower_id из :follower, и followed_id из :followed), но, так как нет ни Followed ни Follower моделей, мы должны снабдить их именем класса User. Результат показан в Листинге 11.6. Обратите внимание, что, в отличие от дефолтно сгенерированной модели Relationship, в данном случае доступным является только followed_id.

Листинг 11.6. Добавление belongs_to ассоциаций к модели Relationship.
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

Ассоциация followed на самом деле не потребуется до Раздела 11.1.5, но параллельность структуры читатели/читаемые лучше видна при одновременной реализации.

В этой точке тесты из Листинга 11.2 и Листинга 11.3 должны пройти.

$ bundle exec rspec spec/

11.1.3 Валидации

Прежде чем двигаться дальше, мы добавим пару валидаций модели Relationship для комплектности. Тесты (Листинг 11.7) и код приложения (Листинг 11.8) просты.

Листинг 11.7. Тестирование валидаций модели Relationship.
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "when followed id is not present" do
    before { relationship.followed_id = nil }
    it { should_not be_valid }
  end

  describe "when follower id is not present" do
    before { relationship.follower_id = nil }
    it { should_not be_valid }
  end
end
Листинг 11.8. Добавление валидаций модели Relationship.
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

11.1.4 Читаемые пользователи

Теперь мы переходим к сердцу ассоциаций Relationship: followed_users и followers. Мы начнем с followed_users, как показано в Листинге 11.9.

Листинг 11.9. Тест для атрибута user.followed_users.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  .
  .
  .
end

Реализация впервые использует has_many through: пользователь имеет много читаемых (пользователей) через взаимоотношения, как показано на Рис. 11.7. По умолчанию, в ассоциации has_many through Rails ищет внешний ключ, соответствующий ассоциации в единственном числе; другими словами, код

has_many :followeds, through: :relationships

будет составлять массив, используя followed_id в таблице relationships. Но, как отмечалось в Разделе 11.1.1, user.followeds это довольно неуклюже; гораздо более естественным будет использование “followed users” в качестве множественного числа для “followed”, и написание user.followed_users для массива читаемых пользователей. Естественно, Rails позволяет переопределить умолчание, в данном случае с помощью :source параметра (Листинг 11.10), который явно говорит Rails, что источником массива followed_users является множество followed ids.

Листинг 11.10. Добавление к модели User ассоциации followed_users.
app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  has_many :followed_users, through: :relationships, source: :followed
  .
  .
  .
end

Для того, чтобы создавать взаимоотношение с читаемым (пользователем), мы введем служебный метод follow! с тем чтобы мы могли написать user.follow!(other_user). (Этот метод follow! должен работать всегда, так что, как и с create! и с save!, мы обозначаем восклицательным знаком что при неудачном создании будет брошено исключение.) Мы также добавим связанный булев метод following? для того чтобы иметь возможность проверять - читает ли пользователь сообщения других пользователей.7 Тесты в Листинге 11.11 показывают как мы планируем использовать эти методы на практике.

Листинг 11.11. Тесты для некоторых служебных методов “following”.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:followed_users) }
  it { should respond_to(:following?) }
  it { should respond_to(:follow!) }
  .
  .
  .
  describe "following" do
    let(:other_user) { FactoryGirl.create(:user) }
    before do
      @user.save
      @user.follow!(other_user)
    end

    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }
  end
end

В коде приложения, метод following? принимает пользователя, названного other_user и проверяет, существует ли он в базе данных; метод follow! вызывает create! через relationships ассоциацию для создания взаимоотношения с читаемым. Результаты представлены в Листинге 11.12.

Листинг 11.12. Служебные методы following? и follow!.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    .
    .
    .
  end

  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end
  .
  .
  .
end

Отметим, что в Листинге 11.12 мы опустили самого пользователя, написав просто

relationships.create!(...)

вместо эквивалентного кода

self.relationships.create!(...)

Явное включение или невключение self в данном случае дело вкуса.

Конечно, пользователи должны иметь возможность прекратить слежение за сообщениями других пользователей, что приводит нас к немного предсказуемому методу unfollow!, как показано в Листинге 11.13.

Листинг 11.13. Тест для прекращения слежения за сообщениями пользователя.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:follow!) }
  it { should respond_to(:unfollow!) }
  .
  .
  .
  describe "following" do
    .
    .
    .
    describe "and unfollowing" do
      before { @user.unfollow!(other_user) }

      it { should_not be_following(other_user) }
      its(:followed_users) { should_not include(other_user) }
    end
  end
end

Код для unfollow! прост: нужно просто найти взаимоотношение по followed id и уничтожить его (Листинг 11.14).

Листинг 11.14. Прекращение слежения за сообщениями пользователя посредством уничтожения взаимоотношения.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end

  def unfollow!(other_user)
    relationships.find_by(followed_id: other_user.id).destroy!
  end
  .
  .
  .
end

11.1.5 Читатели пользователя

Последней частью пазла взаимоотношений является метод user.followers сопутствующий user.followed_users. Вы могли заметить в Рис. 11.7 что все сведения, необходимые для извлечения массива читателей уже присутствуют в таблице relationships. Действительно, техника та же, что и для читаемых пользователей, но с реверсированием ролей follower_id и followed_id. Это говорит о том, что, если бы мы смогли как-то организовать таблицу reverse_relationships, поменяв местами эти два столбца (Рис. 11.9), то мы бы с легкостью реализовали user.followers.

user_has_many_followers_2nd_ed
Рисунок 11.9: Модель данных для читателей пользователя, использующая реверсированную модель Relationship.

Начнем с тестов, веря, что магия Rails выручит нас (когда дело дойдет до реализации) (Листинг 11.15).

Листинг 11.15. Тестирование перевернутых взаимоотношений.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  it { should respond_to(:reverse_relationships) }
  it { should respond_to(:followers) }
  .
  .
  .

  describe "following" do
    .
    .
    .
    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }

    describe "followed user" do
      subject { other_user }
      its(:followers) { should include(@user) }
    end
    .
    .
    .
  end
end

Обратите внимание на то, как мы изменили субъект с помощью метода subject, замена @user на other_user, позволяет нам протестировать взаимоотношение с читателями естесственным образом:

subject { other_user }
its(:followers) { should include(@user) }

Как вы наверное подозреваете, мы не будем создавать полную таблицу в базе данных только для того чтобы просто произвести реверс взаимоотношений. Вместо этого мы воспользуемся базовой симметрией между читаемыми и читателями для симуляции таблицы reverse_relationships, передав followed_id в качестве внешнего ключа. Иными словами, там где ассоциация relationships использует внешний ключ follower_id,

has_many :relationships, foreign_key: "follower_id"

ассоциация reverse_relationships использует followed_id:

has_many :reverse_relationships, foreign_key: "followed_id"

Ассоциация followers затем строится через реверсированные взаимоотношения, как показано в Листинге 11.16.

Листинг 11.16. Реализация user.followers использующая реверсированные взаимоотношения.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  has_many :reverse_relationships, foreign_key: "followed_id",
                                   class_name:  "Relationship",
                                   dependent:   :destroy
  has_many :followers, through: :reverse_relationships, source: :follower
  .
  .
  .
end

(Как и с Листингом 11.4, тест для dependent :destroy остается в качестве упражнения (Раздел 11.5).) Обратите внимание, что мы должны включить имя класса для этой ассоциации, т.e.,

has_many :reverse_relationships, foreign_key: "followed_id",
                                 class_name: "Relationship"

потому что иначе Rails будет искать несуществующий класс ReverseRelationship.

Стоит также отметить, что мы могли бы в этом случае пропустить :source, используя просто

has_many :followers, through: :reverse_relationships

поскольку Rails будет автоматически искать внешний ключ follower_id в данном случае. Я сохранил ключ :source для того чтобы подчеркнуть параллельность со структурой ассоциации has_many :followed_users, но вы можете пропустить его.

С кодом в Листинге 11.16, ассоциации читаемые/читатели завершены, и все тесты должны пройти:

$ bundle exec rspec spec/

Этот раздел предъявил довольно высокие требования к вашим навыкам моделирования данных, и это нормально, если для его усвоения потребуется некоторое время. Фактически, одним из самых лучших способов понять ассоциации является их использование в веб интерфейсе, как мы увидим в следующем разделе.

11.2 Веб-интерфейс для читаемых пользователей

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

11.2.1 Образцы данных

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

Когда мы последний раз видели заполнитель образцов данных в Листинге 10.20, он был немного суматошным. Поэтому мы начнем с определения отдельных методов для создания пользователей и микросообщений, а затем добавим образцы данных взаимоотношений используя новый метод make_relationships. Результаты показаны в Листинге 11.17.

Листинг 11.17. Добавление читатели/читаемые взаимоотношений в образцы данных.
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    make_users
    make_microposts
    make_relationships
  end
end

def make_users
  admin = User.create!(name:     "Example User",
                       email:    "[email protected]",
                       password: "foobar",
                       password_confirmation: "foobar",
                       admin: true)
  99.times do |n|
    name  = Faker::Name.name
    email = "example-#{n+1}@railstutorial.org"
    password  = "password"
    User.create!(name:     name,
                 email:    email,
                 password: password,
                 password_confirmation: password)
  end
end

def make_microposts
  users = User.all(limit: 6)
  50.times do
    content = Faker::Lorem.sentence(5)
    users.each { |user| user.microposts.create!(content: content) }
  end
end

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

Здесь образцы взаимоотношений создаются с помощью кода

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

Мы несколько произвольно организовали слежение первого пользователя за сообщениями пользователей с 3 по 51, а затем принудили пользователей с 4 по 41 читать сообщения первого пользователя. Полученных в результате взаимоотношений будет вполне достаточно для разработки интерфейса приложения.

Чтобы выполнить код Листинга 11.17, заполним базу данных как обычно:

$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake test:prepare

11.2.2 Статистика и форма для слежения за сообщениями пользователя

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

Как было отмечено в Разделе 11.1.1, слово “following” является двусмысленным при использовании в качестве атрибута (где user.following могло бы означать или читаемых пользователей или читателей пользователя), но оно вполне подходит в качестве метки, как в “50 following”. Кроме того, это метка которой пользуется сам Twitter, использование принятое в набросках начиная с Рис. 11.1 и показанное крупным планом на Рис. 11.10.

stats_partial_mockup
Рисунок 11.10: Набросок партиала статистики.

Статистика на Рис. 11.10 содержит и количество читаемых пользователей, и количество его читателей, каждое из чисел должно быть ссылкой на соответствующую специальную страницу для их отображения. В Главе 5, мы временно заглушали подобные ссылки знаком ’#’, но это было до того, как мы набрались опыта с маршрутами. Сейчас, несмотря на то, что мы отложили сами страницы до Раздела 11.2.3, мы сделаем маршруты сейчас, как показано в Листинге 11.18. Этот код использует метод :member внутри блока resources, с которым мы ранее не были знакомы, но посмотрим, сможете ли вы угадать, что он делает. (Примечание: код в Листинге 11.18 должен заменить resources :users.)

Листинг 11.18. Добавление действий following и followers в контроллер Users.
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users do
    member do
      get :following, :followers
    end
  end
  .
  .
  .
end

Вы возможно догадываетесь, что URL для читаемых и читающих пользователей будут выглядеть как /users/1/following и /users/1/followers, и это именно то, что делает код в Листинге 11.18. Поскольку обе страницы будут отображать данные, мы используем get для того чтобы организовать ответ URL на запросы GET (что требуется конвенцией REST для подобных страниц), и метод member означает, что маршруты отвечают на URL, содержащие id пользователя. (Другой возможный метод, collection, работает без id, так что

resources :users do
  collection do
    get :tigers
  end
end

будет отвечать на URL /users/tigers (presumably to display all the tigers in our application).  — предположительно для отображения всех тигров нашего приложения. Узнать больше об этой опции можно из Ruby on Rails по-русски “Роутинг в Rails”. Таблица маршрутов, сгенерированных Листингом 11.18 представлена в Таблице 11.1; обратите внимание на именнованные маршруты для страниц с читателями и читаемыми, которые мы вскоре будем использовать. Неудачное гибридное применение в маршруте “following” обусловлено нашим решением использовать недвусмысленную терминологию “followed users” наряду с применением “following” взятым у Twitter. Поскольку предыдущий привел бы нас к маршрутам вида followed_users_user_path, что звучит довольно странно, мы выбрали последний в контексте Таблицы 11.1, что привело к following_user_path.

HTTP requestURLДействиеИменованный маршрут
GET/users/1/followingfollowingfollowing_user_path(1)
GET/users/1/followersfollowersfollowers_user_path(1)
Table 11.1: RESTful маршруты предоставленные кастомными правилами в ресурсе из Листинга 11.18.

Определив маршруты мы готовы сделать тесты партиала статистики. (Мы могли бы начать с тестов, но именованные маршруты было бы трудно объяснить без обновленного файла маршрутов.) Партиал появится на странице профиля и на странице Home;

Листинг 11.19. Тестирование статистики читатели/читаемые на Home странице.
spec/requests/static_pages_spec.rb
require 'spec_helper'

describe "Static pages" do
  .
  .
  .
  describe "Home page" do
    .
    .
    .
    describe "for signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        FactoryGirl.create(:micropost, user: user, content: "Lorem")
        FactoryGirl.create(:micropost, user: user, content: "Ipsum")
        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

      describe "follower/following counts" do
        let(:other_user) { FactoryGirl.create(:user) }
        before do
          other_user.follow!(user)
          visit root_path
        end

        it { should have_link("0 following", href: following_user_path(user)) }
        it { should have_link("1 followers", href: followers_user_path(user)) }
      end
    end
  end
  .
  .
  .
end

Ядром этих тестов является предположение, что количество читателей и читаемых представлено на странице совместно с правильными URL:

it { should have_link("0 following", href: following_user_path(user)) }
it { should have_link("1 followers", href: followers_user_path(user)) }

Здесь мы использовали именованные маршруты, показанные в Таблице 11.1 для проверки того, что ссылки имеют правильные адреса. Также обратите внимание на то, что в данном случае слово “followers” работает как метка, так что мы сохраним его во множественном числе, даже если будет только один читатель.

Код приложения для партиала статистики это просто несколько ссылок внутри div, как показано в Листинге 11.20.

Листинг 11.20. Партиал для отображения статистики.
app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.followed_users.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

Поскольку мы включим статистику и на профиле пользователя и на Home странице, первая строка Листинга 11.20 выбирает правильное с помощью

<% @user ||= current_user %>

Как обсуждалось в Блоке 8.2, если @user не является nil, то ничего не происходит (как и на странице профиля), но когда он существует (как на странице Home), он назначает @user равным текущему пользователю.

Обратите внимание также на то, что количество читаемых/читателей подсчитывается через ассоциацию с помощью

@user.followed_users.count

и

@user.followers.count

Сравните это с подсчетом количества микросообщений из Листинга 10.17, где мы писали

@user.microposts.count

для подсчета микросообщений.

Одна последняя деталь которую стоит отметить - наличие CSS id у некоторых элементов, как в

<strong id="following" class="stat">
...
</strong>

Это сделано в угоду Ajax реализации из Раздела 11.2.5, которая получает доступ к элементам страницы используя их уникальные id.

С готовым партиалом включить статистику в Home страницу проще простого, как показано в Листинге 11.21. (Это также приводит к прохождению тестов из Листинга 11.19.)

Листинг 11.21. Добавление статистики к Home странице.
app/views/static_pages/home.html.erb
<% if signed_in? %>
      .
      .
      .
      <section>
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/stats' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
      .
      .
      .
<% else %>
  .
  .
  .
<% end %>

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

Листинг 11.22. SCSS для боковой панели страницы Home.
app/assets/stylesheets/custom.css.scss
.
.
.

/* sidebar */
.
.
.
.stats {
  overflow: auto;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $grayLighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: $blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
}
.
.
.
home_page_follow_stats_bootstrap
Рисунок 11.11: Home страница (/) со статистикой. (полный размер)

Мы подключим статистику к странице профиля через мгновение, но вначале давайте сделааем партиал для follow/unfollow кнопки, как показано в Листинге 11.23.

Листинг 11.23. Партиал для формы follow/unfollow.
app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

Этот партиал ничего не делает кроме перекладывания реальной работы на follow и unfollow партиалы, которым нужен новый файл маршрутов чьи правила для ресурса Relationships, следуют примеру ресурса Microposts (Листинг 10.22), как показано в Листинге 11.24.

Листинг 11.24. Добавление маршрутов для пользовательских взаимоотношений.
config/routes.rb
SampleApp::Application.routes.draw do
  .
  .
  .
  resources :sessions,      only: [:new, :create, :destroy]
  resources :microposts,    only: [:create, :destroy]
  resources :relationships, only: [:create, :destroy]
  .
  .
  .
end

Сами партиалы follow/unfollow показаны в Листинге 11.25 и Листинге 11.26.

Листинг 11.25. Форма для слежения за сообщениями пользователя.
app/views/users/_follow.html.erb
<%= form_for(current_user.relationships.build(followed_id: @user.id)) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>
Листинг 11.26. Форма для отписки от сообщений пользователя.
app/views/users/_unfollow.html.erb
<%= form_for(current_user.relationships.find_by(followed_id: @user),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

Обе эти формы используют form_for для манипуляций с объектом модели Relationship; основное отличие между ними заключается в том, что Листинг 11.25 строит новое взаимоотношение, тогда как Листинг 11.26 ищет существующее взаимоотношение. Естественно, первый отправляет POST запрос к контроллеру Relationships для create взаимоотношения, в то время как последний отправляет DELETE запрос для destroy взаимоотношения. (Мы напишем эти действия в Разделе 11.2.4.) Наконец, отметьте что форма follow/unfollow не содержит никакого контента кроме кнопки, но нам все еще необходимо отправить followed_id, чего мы можем добиться с помощью метода hidden_field из Листинга 11.25; который производит HTML вида

<input id="relationship_followed_id"
name="relationship[followed_id]"
type="hidden" value="3" />

Тег “hidden” input поместит соответствующую информацию на странице не отображая ее в браузере.

Теперь мы можем включить форму чтения и статистику на страницу профиля пользователя простым рендерингом партиалов, как показано в Листинге 11.27. Профили с follow и unfollow кнопками, соответственно, представлены на Рис. 11.12 и Рис. 11.13.

Листинг 11.27. Добавление формы слежения за сообщениями пользователя и статистики на страницу профиля пользователя.
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="span4">
    <section>
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section>
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="span8">
    <%= render 'follow_form' if signed_in? %>
    .
    .
    .
  </div>
</div>
profile_follow_button_bootstrap
Рисунок 11.12: Профиль пользователя с follow кнопкой (/users/2). (полный размер)
profile_unfollow_button_bootstrap
Рисунок 11.13: Профиль пользователя с unfollow кнопкой (/users/6). (полный размер)

Мы вскоре сделаем эти кнопки рабочими, фактически, мы сделаем это двумя способами, стандартным способом (Раздел 11.2.4) и с помощью Ajax (Раздел 11.2.5), но вначале мы закончим HTML интерфейс, создав страницы для списков читающих и читаемых.

11.2.3 Страницы с читаемыми и читателями

Страницы для отображения читающих сообщения пользователя и читаемых им пользователей будут напоминать гибрид страницы профиля пользователя и страницы со списком пользователей (Раздел 9.3.1), с сайдбаром пользовательской информации (включая статистику слежения за сообщениями) и таблицу пользователей. Кроме того, мы включим сетку пользовательских профильных изображений-ссылок в сайдбаре. Набросок соответствующий этим требованиям представлен на Рис. 11.14 (читаемые) и Рис. 11.15 (читатели).

following_mockup_bootstrap
Рисунок 11.14: Набросок страницы со списком читаемых пользователей. (полный размер)
followers_mockup_bootstrap
Рисунок 11.15: Набросок страницы со списком читателей. (полный размер)

Нашим первым шагом будет получение рабочих ссылок following (читаемые) и followers (читатели). Мы будем следовать примеру Твиттера и обе страницы будут требовать входа пользователя, как было протестировано в Листинге 11.28. Для вошедших пользователей, страницы должны иметь ссылки на читаемых и читателей, соответственно, как протестировано в Листинге 11.29.

Листинг 11.28. Тестирование авторизации страниц читаемых и читателей.
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 Users controller" do
        .
        .
        .
        describe "visiting the following page" do
          before { visit following_user_path(user) }
          it { should have_title('Sign in') }
        end

        describe "visiting the followers page" do
          before { visit followers_user_path(user) }
          it { should have_title('Sign in') }
        end
      end
      .
      .
      .
    end
    .
    .
    .
  end
  .
  .
  .
end
Листинг 11.29. Тесты для followed_users и followers страниц.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "following/followers" do
    let(:user) { FactoryGirl.create(:user) }
    let(:other_user) { FactoryGirl.create(:user) }
    before { user.follow!(other_user) }

    describe "followed users" do
      before do
        sign_in user
        visit following_user_path(user)
      end

      it { should have_title(full_title('Following')) }
      it { should have_selector('h3', text: 'Following') }
      it { should have_link(other_user.name, href: user_path(other_user)) }
    end

    describe "followers" do
      before do
        sign_in other_user
        visit followers_user_path(other_user)
      end

      it { should have_title(full_title('Followers')) }
      it { should have_selector('h3', text: 'Followers') }
      it { should have_link(user.name, href: user_path(user)) }
    end
  end
end

Единственно сложная часть реализации это осуществление потребности добавления двух новых действий к контроллеру Users; основанных на маршрутах, определенных в Листинге 11.18, нам нужно назвать их following и followers. Каждому действию нужно установить заголовок, найти пользователя, вытянуть @user.followed_users или @user.followers (в пагинированной форме), а затем отренедерить страницу. Результат представлен в Листинге 11.30.

Листинг 11.30. Действия following и followers.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user,
                only: [:index, :edit, :update, :destroy, :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user = User.find(params[:id])
    @users = @user.followed_users.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end

Отметим здесь, что оба действия делают явный вызов render, в данном случае делая рендеринг представления, названного show_follow, которое мы должны создать. Причина создания общего представления в том, что ERb является практически идентичным для обоих случаев, и Листинг 11.31 охватывает их.

Листинг 11.31. Представление show_follow используемое для рендеринга читаемых и читателей.
app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="span4">
    <section>
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section>
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="span8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

С этим тесты должны пройти, а страницы должны выглядеть как на Рис. 11.16 (читаемые) и Рис. 11.17 (читатели).

user_following_bootstrap
Рисунок 11.16: Отображение пользователей, читаемых текущим пользователем. (полный размер)
user_followers_bootstrap
Рисунок 11.17: Отображение читателей текущего пользователя. (полный размер)

11.2.4 Стандартный способ реализации кнопки "читать" (follow)

Теперь, когда наши представления в порядке, пришло время получить рабочие follow/unfollow кнопки. Тесты для этих кнопок комбинируют множество техник тестирования о которых было рассказано в этом учебнике и представляют из себя хорошее упражнение в чтении кода. Изучайте Листинг 11.32 до тех пор пока не убедитесь что вы понимаете что и почему мы тестируем. (Листинг 11.32 содержит одно небольшое упущение в безопасности; посмотрим, сможете ли вы выявить его. Мы скоро расскажем о нем.) Особенно обратите внимание на использование метода have_xpath использующего продвинутую и мощную технику XPath для навигации по XML документам (включая HTML5). Вы можете узнать больше о XPath с помощью поискового запроса XPath syntax.

Листинг 11.32. Тесты для Follow/Unfollow кнопки.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "profile page" do
    let(:user) { FactoryGirl.create(:user) }
    .
    .
    .
    describe "follow/unfollow buttons" do
      let(:other_user) { FactoryGirl.create(:user) }
      before { sign_in user }

      describe "following a user" do
        before { visit user_path(other_user) }

        it "should increment the followed user count" do
          expect do
            click_button "Follow"
          end.to change(user.followed_users, :count).by(1)
        end

        it "should increment the other user's followers count" do
          expect do
            click_button "Follow"
          end.to change(other_user.followers, :count).by(1)
        end

        describe "toggling the button" do
          before { click_button "Follow" }
          it { should have_xpath("//input[@value='Unfollow']") }
        end
      end

      describe "unfollowing a user" do
        before do
          user.follow!(other_user)
          visit user_path(other_user)
        end

        it "should decrement the followed user count" do
          expect do
            click_button "Unfollow"
          end.to change(user.followed_users, :count).by(-1)
        end

        it "should decrement the other user's followers count" do
          expect do
            click_button "Unfollow"
          end.to change(other_user.followers, :count).by(-1)
        end

        describe "toggling the button" do
          before { click_button "Unfollow" }
          it { should have_xpath("//input[@value='Follow']") }
        end
      end
    end
  end
  .
  .
  .
end

Листинг 11.32 тестирует кнопки кликая по ним и проверяя соответствующее поведение. Написание реализации подразумевает чуть более глубокое погружения в тему: following и unfollowing включает создание и уничтожение взаимоотношений, что означает необходимость определения create и destroy действий в контроллере Relationships (который нам еще нужно создать). Хотя кнопки появляются только для вошедших пользователей, что дает нам безопасность верхнего уровня, тесты в Листинге 11.32 упускают из виду безопасность на уровне контроллера, а именно: сами create и destroy должны быть доступны только для вошедших пользователей. (Это та самая уязвимость о которой мы говорили выше.) Листинг 11.33 выражает эти требования с помощью post и delete методов вызывающих эти действия напрямую.

Листинг 11.33. Тестирование авторизации контроллера Relationships.
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 Relationships controller" do
        describe "submitting to the create action" do
          before { post relationships_path }
          specify { expect(response).to redirect_to(signin_path) }
        end

        describe "submitting to the destroy action" do
          before { delete relationship_path(1) }
          specify { expect(response).to redirect_to(signin_path) }
        end
      end
      .
      .
      .
    end
  end
end

Обратите внимание, что, для того чтобы избежать лишней работы по созданию практически бесполезного объекта Relationship, тест delete хардкодит id 1 в именованном маршруте:

before { delete relationship_path(1) }

Это работает из-за того что пользователь должен быть перенаправлен прежде чем приложение даже попытается обратиться к взаимоотношению с этим id.

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

Листинг 11.34. Контроллер Relationships.
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    redirect_to @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    redirect_to @user
  end
end

В Листинге 11.34 мы можем видеть почему уязвимость отмеченная выше является незначительной: если невошедший пользователь попробовал бы обратиться к любому из действий напрямую (например, с помощью инструмента командной строки вроде curl), current_user был бы nil и в обоих случаях вторая строка действий вызвала бы исключение, что привело бы к ошибке, но не нанесло бы вреда приложению или его данным. Однако лучше на это не полагаться, так что мы предприняли дополнительный шаг и добавили дополнительный уровень безопасности.

С этим ядро функциональности follow/unfollow завершено, и любой пользователь может читать (или не читать) сообщения любого другого пользователя, что вам стоит проверить и в вашем браузере и запустив набор тестов:

$ bundle exec rspec spec/

11.2.5 Реализация кнопки "читать" (follow) с Ajax

Хотя наша реализация слежения за сообщениями пользователей является законченной и в своем нынешнем виде, нам осталось совсем немного подправить ее прежде чем заняться потоком сообщений. Вы могли заметить в Разделе 11.2.4 что оба create и destroy действия в контроллере Relationships просто перенаправляют обратно к исходному профилю. Другими словами, пользователь начинает на странице профиля, подписывается на сообщения пользователя и немедленно перенаправляется на исходную страницу. Резонный вопрос - почему пользователь вообще должен покидать эту страницу?

Именно эту проблему решает Ajax, который позволяет веб страницам отправлять асинхронные запросы на сервер не покидая страницы.8 Поскольку практика добавления Ajax в веб формы является довольно распространенной, Rails делает реализацию Ajax легкой. Действительно, обновление партиалов формы follow/unfollow тривиально: просто заменим

form_for

на

form_for ..., remote: true

и Rails автомагически будет использовать Ajax.9 Обновленные партиалы представлены в Листинге 11.35 и Листинге 11.36.

Листинг 11.35. Форма для чтения сообщений пользователя использующая Ajax.
app/views/users/_follow.html.erb
<%= form_for(current_user.relationships.build(followed_id: @user.id),
             remote: true) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>
Листинг 11.36. Форма для прекращения чтения сообщений пользователя использующая Ajax.
app/views/users/_unfollow.html.erb
<%= form_for(current_user.relationships.find_by(followed_id: @user),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

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

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post">
  .
  .
  .
</form>

Это устанавливает переменную data-remote="true" внутри тега формы, что говорит Rails о том, что форма будет обрабатываться JavaScript. Используя простое свойство HTML вместо вставки полного JavaScript кода (как в предыдущих версиях Rails), Rails 3 следует философии ненавязчивого JavaScript.

После обновления формы нам нужно уговорить контроллер Relationships отвечать на Ajax запросы. Тестирование Ajax является довольно сложным, и тщательное делание этого является большой темой со своими собственными правилами, но мы можем начать с кодом в Листинге 11.37. Это использует xhr метод (от “XmlHttpRequest”) для выдачи Ajax запроса; сравните с get, post, patch и delete методами в предыдущих тестах. Затем мы проверяем что create и destroy действия делают правильные вещи когда вызываются Ajax запросом. (Для написания более основательного набора тестов для насыщенных Ajax-ом приложений, взгляните на Selenium и Watir.)

Листинг 11.37. Тесты для ответов контроллера Relationships на Ajax запросы.
spec/controllers/relationships_controller_spec.rb
require 'spec_helper'

describe RelationshipsController do

  let(:user) { FactoryGirl.create(:user) }
  let(:other_user) { FactoryGirl.create(:user) }

  before { sign_in user, no_capybara: true }

  describe "creating a relationship with Ajax" do

    it "should increment the Relationship count" do
      expect do
        xhr :post, :create, relationship: { followed_id: other_user.id }
      end.to change(Relationship, :count).by(1)
    end

    it "should respond with success" do
      xhr :post, :create, relationship: { followed_id: other_user.id }
      expect(response).to be_success
    end
  end

  describe "destroying a relationship with Ajax" do

    before { user.follow!(other_user) }
    let(:relationship) { user.relationships.find_by(followed_id: other_user) }

    it "should decrement the Relationship count" do
      expect do
        xhr :delete, :destroy, id: relationship.id
      end.to change(Relationship, :count).by(-1)
    end

    it "should respond with success" do
      xhr :delete, :destroy, id: relationship.id
      expect(response).to be_success
    end
  end
end

Код в Листинге 11.37 это наш первый пример тестов контроллера, которыми я ранее интенсивно пользовался (например в предыдущем издании этого учебника), но которым я сейчас предпочитаю интеграционные тесты. Однако в данном случае, метод xhr (по непонятным мне причинам) не доступен в интеграционных тестах. Хотя мы впервые используем xhr, в этой точке учебника вы, вероятно, уже можете понять из контекста чем занимается этот код:

xhr :post, :create, relationship: { followed_id: other_user.id }

Мы видим что xhr принимает в качестве аргумента символ для соответствующего метода HTTP, символ для действия и хэш представляющий собой содержимое params в самом контроллере. Как и в предыдущих примерах, мы используем expect для того чтобы обернуть операцию в блок и протестировать увеличение или уменьшение соответсвующего количества.

Как следует из тестов, код приложения использует те же create и delete действия для ответа на Ajax запросы которые он выдает чтобы ответить на обычные POST и DELETE HTTP запросы. Все что нам нужно сделать это ответить на обычный HTTP запрос с переадресацией (как в Разделе 11.2.4) и ответить на Ajax запрос с JavaScript. Код контроллера представлен в Листинге 11.38. (См. в Разделе 11.5 пример показывающий более компактный способ выполнить то же самое.)

Листинг 11.38. Ответ на Ajax запросы в контроллере Relationships.
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

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

respond_to do |format|
  format.html { redirect_to @user }
  format.js
end

выполняется только одна из строк (в зависимости от характера запроса).

В случае Ajax запроса, Rails автоматически вызывает JavaScript Embedded Ruby (.js.erb) файл с тем же именем что и действие, т.е., create.js.erb или destroy.js.erb. Как вы можете догадаться, эти файлы позволяют смешивать JavaScript и Embedded Ruby для выполнения действий на текущей странице. Именно эти файлы нам нужны для создания и редактирования страницы профиля пользователя после начала слежения за сообщениями пользователя или после его прекращения.

Внутри JS-ERb файла, Rails автоматически обеспечивает jQuery JavaScript хелперы для манипуляции страницей при помощи Document Object Model (DOM). Библиотека jQuery предоставляет большое количество методов для манипуляции DOM, но здесь нам понадобятся только два. Во первых мы должны знать о синтаксисе знака доллара, используемого для доступа к DOM элементу опираясь на его уникальный CSS id. Например, для манипуляции элементом follow_form, мы используем синтаксис

$("#follow_form")

(Вспомните из Листинга 11.23 что это div который обертывает форму, а не сама форма.) Этот синтаксис, вдохновленный CSS, использует символ # для указания CSS id. Как вы можете догадаться, jQuery, как и CSS, использует точку . для манипуляций с CSS классами.

Второй метод который нам потребуется это html, который обновляет HTML внутри соответствующего элемента содержимым своего аргумента. Например, чтобы полностью заменить follow form на строку "foobar", мы можем написать

$("#follow_form").html("foobar")

В отличие от простых JavaScript файлов, JS-ERb файлы позволяют также использовать Embedded Ruby, что мы применяем в create.js.erb файле для замены формы на партиал unfollow (это то, что должно быть видно после успешной подписки на сообщения пользователя) и обновления количества читаемых. Результат представлен в Листинг 11.39. Здесь используется функция escape_javascript, которая необходима для маскирования результатов при вставке HTML в файл JavaScript.

Листинг 11.39. JavaScript Embedded Ruby для создания взаимоотношения при чтении сообщений другого пользователя.
app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')

Файл destroy.js.erb аналогичен (Листинг 11.40).

Листинг 11.40. Ruby JavaScript (RJS) для уничтожения взаимоотношения при отказе от чтения сообщений другого пользователя.
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')

Теперь вам следует перейти на страницу профиля пользователя и проверить, что вы можете следить и не следить за сообщениями пользователей без обновления страницы, а набор тестов должен позеленеть:

$ bundle exec rspec spec/

Использование Ajax в Rails является большой и стремительно развивающейся темой, и здесь мы лишь слегка коснулись ее, но (как и с остальными материалами в этом учебнике) наш подход дает вам хорошую основу для изучения более продвинутых ресурсов.

11.3 Поток сообщений

Мы подошли к кульминации нашего примера приложения: потоку сообщений. Соответствено, этот раздел содержит некоторые из самых продвинутых материалов в данном учебнике. Полноценный поток сообщений опирается на свой прототип из Раздела 10.3.3 и собирает массив микросообщений из микросообщений читаемых пользователей, совместно с собственными микросообщениями текущего пользователя. Чтобы совершить этот подвиг, нам понадобятся некоторые довольно продвинутые Rails, Ruby и даже SQL техники программирования.

Так как нам предстоит тяжелая работа, особенно важно понимать куда мы будем двигаться. Резюме окончательного потока сообщений пользователя, показанный на Рис. 11.5, вновь показан на Рис. 11.18.

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

11.3.1 Мотивация и стратегия

Основная идея потока сообщений проста. Рис. 11.19 показывает пример таблицы базы данных microposts и результирующий поток сообщений. Цель потока заключается в вытягивании микросообщений чей user id соответствует пользователям, сообщения которых читает текущий пользователь (и id самого текущего пользователя), как указано стрелками на схеме.

user_feed
Рисунок 11.19: Поток сообщений для пользователя (id 1) читающего сообщения пользователей 2, 7, 8 и 10.

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

Micropost.from_users_followed_by(user)

Хотя мы пока не знаем как реализовать его, мы уже можем написать тесты для его функциональности. Ключевым моментом является проверка всех трех требований к потоку: микросообщения читаемых пользователей и самого пользователя должны быт включены в поток, но в него не должны попадать сообщения пользователей, от чтения которых пользователь отписался. Первые два требования уже представлены в наших тестах: Листинг 10.35 проверяет что собственные микросообщения пользователя представлены в потоке, в то время как микросообщений отписанных пользователей быть не должно. Теперь, когда мы знаем как следить за сообщениями пользователей, мы можем добавить третий тип тестов, в этот раз проверяющих что микросообщения читаемых пользователей представлены в потоке, как это показано в Листинге 11.41.

Листинг 11.41. Финальные тесты для потока сообщений.
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
    .
    .
    .
    describe "status" do
      let(:unfollowed_post) do
        FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
      end
      let(:followed_user) { FactoryGirl.create(:user) }

      before do
        @user.follow!(followed_user)
        3.times { followed_user.microposts.create!(content: "Lorem ipsum") }
      end

      its(:feed) { should include(newer_micropost) }
      its(:feed) { should include(older_micropost) }
      its(:feed) { should_not include(unfollowed_post) }
      its(:feed) do
        followed_user.microposts.each do |micropost|
          should include(micropost)
        end
      end
    end
  end
  .
  .
  .
end

Сам поток сообщений просто перекладывает тяжелую работу на Micropost.from_users_followed_by, как это показано в Листинге 11.42.

Листинг 11.42. Добавление завершенного потока сообщений к модели User.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    Micropost.from_users_followed_by(self)
  end
  .
  .
  .
end

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

Пришло время реализовать Micropost.from_users_followed_by, который мы для простоты будем называть “поток”. Поскольку конечный результат довольно сложен, мы будем строить итоговую реализацию потока по кусочкам.

В первую очередь нужно подумать о том, какой вид запроса нам нужен. Что мы хотим сделать, это выбрать из таблицы microposts все микросообщения с id соответствующими пользователям, читаемыми данным пользователем (или самому пользователю). Мы можем схематически написать это следующим образом:

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

При написании этого кода мы предположили, что SQL поддерживает ключевое слово IN, что позволяет нам протестировать множественное включение. (К счастью это так.)

Вспомним из предварительной реализации потока в Разделе 10.3.3 что Active Record использует where метод для осуществления вида выбора, показанного выше, что иллюстрирует Листинг 10.36. Там наша выборка была очень простой; мы просто взяли все микросообщения с user id соответствующим текущему пользователю:

Micropost.where("user_id = ?", id)

Здесь мы ожидаем, что он будет более сложным, чем то вроде

where("user_id in (?) OR user_id = ?", following_ids, user)

(Здесь мы в состоянии использовать, согласно Rails конвенции, user вместо user.id; Rails автоматически использует id. Мы также опустили впереди идущую часть Micropost. поскольку мы ожидаем что этот метод будет жить в самой модели Micropost.)

Мы видим из этих условий, что нам нужен массив id пользователей, читаемых данным пользователем (или какой-то эквивалент). Один из способов сделать это заключается в использовании Ruby метода map, доступного на любом “перечисляемом” объекте, т.е., любом объекте (таком как Массив или Хэш), который состоит из коллекции элементов.10 Мы видели пример этого метода в Разделе 4.3.2; он работает следующим образом:

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

Ситуации, подобные той, что показана выше, где такой же метод (например, to_s) вызывается на каждый элемент, настолько обычная вещь, что есть сокращенная запись, использующая ампресанд & и символ, соответствующий методу:11

>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]

Используя метод join (Раздел 4.3.1), мы можем создать строку состоящую из id объединив их через запятую-пробел:

>> [1, 2, 3, 4].map(&:to_s).join(', ')
=> "1, 2, 3, 4"

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

>> User.first.followed_users.map(&:id)
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Фактически, так как конструкции такого вида очень полезны, Active Record обеспечивает ее по умолчанию:

>> User.first.followed_user_ids
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Здесь метод followed_user_ids синтезирован библиотекой Active Record на основе ассоциации has_many :followed_users (Листинг 11.10); в результате, для получения id соответствующих коллекции user.followed_users, нам достаточно добавить _ids к названию ассоциации. Строка id читаемых пользователей тогда будет выглядеть следующим образом:

>> User.first.followed_user_ids.join(', ')
=> "4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51"

Однако при вставке строки в SQL, вам нет надобности делать этого; интерполяция ? заботится об этом за вас (и фактически устраняет некоторые несовместимости связанные с базой данных). Это означает что мы можем использовать

user.followed_user_ids

само по себе.

В этой точке вы можете догадаться что код вроде

Micropost.from_users_followed_by(user)

будет включать в себя метод класса в Micropost классе (конструкция кратко упоминавшаяся в Разделе 4.4.1). Предполагаемая реализация с этими строками представлена в Листинге 11.43.

Листинг 11.43. Первый дубль для from_users_followed_by метода.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (?) OR user_id = ?", followed_user_ids, user)
  end
end

Хотя обсуждение ведущее к Листингу 11.43 было выдержано в гипотетических тонах, он действительно работает! Вы можете проверить это запустив набор тестов, которые должны пройти:

$ bundle exec rspec spec/

В большинстве приложений этой начальной реализации было бы вполне достаточно для большинства практических целей. Но это не финальная реализация; посмотрим, сможете ли вы догадаться почему, прежде чем перейти к следующему разделу. (Намек: А что если пользователь следит за сообщениями 5000 других пользователей?)

11.3.3 Подзапросы

Как намекалось в последнем разделе, реализация потока сообщений в Section 11.3.2 не очень хорошо масштабируется при большом количестве микросообщений, что скорее всего произойдет если пользователь начнет читать сообщения, скажем, 5000 других пользователей. В этом разделе мы повторно реализуем ленту сообщений способом, который лучше масштабируется с количеством читаемых пользователей.

Проблема с кодом из Раздела 11.3.2 в том что

followed_user_ids = user.followed_user_ids

вытягивает всех читаемых пользователей в память и создает массив длинной во весь список читаемых пользователей. Поскольку условие в Листинге 11.43 на самом деле лишь проверяет включение во множество, должен быть более эффективный способ для этого, да и SQL оптимизирован именно для таких множественных операций. Решение заключается в отправке поиска id читаемых пользователей в базу данных с помощью подзапроса.

Мы начнем с рефакторинга потока немного модифицированным кодом в Листинге 11.44.

Листинг 11.44. Улучшение from_users_followed_by.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  # Returns microposts from the users being followed by the given user.
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (:followed_user_ids) OR user_id = :user_id",
          followed_user_ids: followed_user_ids, user_id: user)
  end
end

В качестве подготовки к следующему шагу мы заменили

where("user_id IN (?) OR user_id = ?", followed_user_ids, user)

на эквивалентное

where("user_id IN (:followed_user_ids) OR user_id = :user_id",
      followed_user_ids: followed_user_ids, user_id: user)

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

Обсуждение выше привело нас к тому что мы добавим второе вхождение user_id в SQL запросе. В частности, мы можем заменить Ruby код

followed_user_ids = user.followed_user_ids

на фрагмент SQL

followed_user_ids = "SELECT followed_id FROM relationships
                     WHERE follower_id = :user_id"

Этот код содержит подзапрос SQL и внутренне вся выборка для пользователя 1 будет выглядеть примерно так:

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE follower_id = 1)
      OR user_id = 1

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

С этим фундаментом мы готовы к эффективной релизации потока сообщений, как видно в Листинге 11.45. Обратите внимание, что, так как теперь это чистый SQL, followed_user_ids является интерполированным, а не маскированным. (На самом деле рабочими являются оба варианта, но мне кажется более логичным интерполировать в данном контексте.)

Листинг 11.45. Финальная реализация from_users_followed_by.
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

  # Returns microposts from the users being followed by the given user.
  def self.from_users_followed_by(user)
    followed_user_ids = "SELECT followed_id FROM relationships
                         WHERE follower_id = :user_id"
    where("user_id IN (#{followed_user_ids}) OR user_id = :user_id",
          user_id: user.id)
  end
end

Этот код представляет собой внушительную комбинацию Rails, Ruby, и SQL, но он делает свою работу и делает ее хорошо. (Конечно же, даже подзапрос не является универсальным решением для масштабирования. Для бОльших сайтов, вам, вероятно, потребуется генерировать поток асинхронно с помощью фонового процесса. Такие тонкости масштабирования выходят за рамки данного руководства.)

11.3.4 Новый поток сообщений

С кодом в Листинге 11.45, наш поток сообщений завершен. Напомним, что код для Home страницы, представлен в Листинге 11.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в Рис. 11.20.13 Отметим, что paginate метод фактически достигает цели в методе модели Micropost в Листинге 11.45, организуя вытягивание только 30 микросообщений за раз из базы данных. (Вы можете проверить это изучив SQL выражения в логах сервера разработки.)

Листинг 11.46. Действие 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
home_page_with_feed_bootstrap
Рисунок 11.20: Home страница с работающим потоком сообщений. (полный размер)

11.4 Заключение

Добавив ленту сообщений, мы закончили ключевой пример приложения Учебника Ruby on Rails. Это приложение включает в себя примеры всех основных возможностей Rails, включая модели, представления, контроллеры, шаблоны, партиалы, фильтры, валидации, обратные вызовы, has_many/belongs_to и has_many through ассоциации, безопасность, тестирование и развертывание. Несмотря на этот внушительный список, вам предстоит еще очень многое узнать о Rails. В качестве первого шага на этом пути, этот раздел содержит некоторые рекомендуемые расширения основного приложения, а также рекомендации для дальнейшего обучения.

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

$ git add .
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

Как обычно, если хотите, вы можете также отправить ваше приложение и развернуть его на сервере:

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:populate

11.4.1 Расширения к примеру приложения

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

Не удивляйтесь, если по началу будет непросто; чистый лист новой фичи может быть немного пугающим. Чтобы помочь вам начать, я могу дать две большие рекомендации. Во-первых, пежде чем добавлять какую-либо функцию к Rails приложению, взгляните на RailsCasts archive чтобы посмотреть, не рассказал ли уже об этом Ryan Bates.14 Если он это сделал, просмотр соответствующего Railscast сэкономит вам массу времени. Во-вторых, всегда делайте обширный поиск в Google по вашей предполагаемой функции, чтобы найти соответствующее сообщения в блогах и пособиях. Разработка веб приложений это непростое дело и это поможет вам учиться на чужом опыте (и ошибках).

Многие из следующих функций являются довольно непростыми задачами, и я дал несколько подсказок по поводу средств, которые могут вам понадобиться для их реализации. Даже с подсказками, они являются намного более трудными чем упражнения, которые приводились в конце каждой главы учебника, так что не расстраивайтесь, если вы не можете решить их без значительных усилий. Из-за нехватки времени я недоступен для личной помощи, но если есть достаточный интерес я мог бы выпустить автономную статью/скринкаст охватывающий эти расширения в будущем; перейдите на основной сайт Rails Tutorial https://railstutorial.org и подпишитесь на ленту новостей, чтобы быть в курсе последних обновлений.

Реплики

Твиттер позволяет пользователям делать “@replies”, которые являются микросообщениями, чьи первые символы являются логином пользователя предшествующим знаку @. Эти сообщения появляются только в потоке сообщений у пользователя задавшего вопрос или у пользователей читающих данного пользователя. Реализуйте упрощенную версию этого, ограничив появление @replies только в потоках сообщений получателя и отправителя. Это может подразумевать добавление in_reply_to столбца в таблицу microposts и дополнительного including_replies пространства к модели Micropost

Поскольку нашему приложению не хватает уникальных пользовательских логинов, вам также необходимо решить, каким способом представлять пользователей. Один из вариантов это использование комбинации id и имени, например @1-michael-hartl. Другой способ это добавить уникальное имя пользователя в процесс регистрации и затем использовать его в @replies.

Обмен сообщениями

Твиттер поддерживает непосредственный (приватный) обмен сообщениями с помощью добавления префикса с буквой “d” к микросообщению. Реализуйте эту функцию для примера приложения. Решение, вероятно, подразумевает наличие модели Message и проверку новых микросообщений с помощью регулярных выражений.

Уведомления о новых читателях

Реализуйте функцию, отправляющую каждому пользователю email уведомление когда у него появляется новый читатель. Затем сделайте уведомления необязательными, так чтобы пользователи могли отказаться при желании. Помимо всего прочего, добавление этой функции требует знания о том, как отправлять почту с помощью Rails. Начните с RailsCast on Action Mailer in Rails 3.

Напоминание пароля

В настоящее время, если пользователи нашего приложения забудут свои пароли, они не смогут их восстановить. Из-за одностороннего безопасного хэширования паролей в Главе 6, наше приложение не может отправить по email пароли пользователей, но оно может отправить ссылку на форму сброса пароля. Используя RailsCast on Remember Me & Reset Password в качестве примера, исправьте это упущение.

Подтверждение регистрации

Помимо регулярного выражения для электронной почты, пример приложения в настоящее время не имеет способа проверки валидности пользовательского email адреса. Добавьте шаг проверки email адреса в подтверждение регистрации пользователя. Новая функция должна создавать пользователей в неактивном состоянии, отправлять по email пользователям активационный URL, а затем активировать статус пользователя при посещении соответствующего URL. Для работы с активный/неактивный переходами вам может помочь прочтение state machines in Rails.

RSS канал

Реализовать для каждого пользователя RSS канал их микросообщений. Затем реализовать RSS канал для их лент сообщений, опционально ограничив доступ к этому каналу используя аутентификационную схему. RailsCast on generating RSS feeds поможет вам начать.

REST API

Многие веб сайты раскрывают Application Programmer Interface (API) так что сторонние приложения могут get, post, put и delete ресурсы приложения. Реализуйте такой REST API для примера приложения. Решение подразумевает добавление respond_to блоков (Раздел 11.2.5) ко многим действиям Application контроллера; они должны отвечать на запросы для XML. Позаботьтесь о безопасности; API должен быть доступен только авторизированным пользователям.

Поиск

В настоящее время у пользователей нет другого способа найти друг-друга, кроме как просмотром списка пользователей или просматривая потоки сообщений других пользователей. Реализуйте функцию поиска чтобы исправить ситуацию. Затем добавьте другую поисковую функцию для микросообщений. RailsCast on simple search forms поможет вам начать. Если вы используете шаред хостинг или выделенный сервер, я советую использовать Thinking Sphinx (см. RailsCast on Thinking Sphinx). Если вы развернуты на Heroku, вы должны следовать инструкциям Heroku full text search.

11.4.2 Руководство по дальнейшим ресурсам

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

  • The Ruby on Rails Tutorial screencasts: я подготовил полноценный скринкаст курс основанный на этой книге. В дополнение к раскрытию всех материалов этой книги, скринкасты дополнены советами, трюками и демонстрациями типа смотри-как-это-делается, которые сложно зафиксировать в печатном варианте. Их можно приобрести через сайт Ruby on Rails Tutorial.
  • RailsCasts: трудно переоценить важность ресурса Railscasts, я советую начать с посещения архива эпизодов Railscasts и клика по любой зацепившей вас теме.
  • Ruby и Rails книги: я рекомендую Beginning Ruby Петера Купера, The Well-Grounded Rubyist David A. Black, Eloquent Ruby Russ Olsen и The Ruby Way Хэла Фултона для дальнейшего изучения Ruby, и The Rails 3 Way Оби Фернандеса, и Rails 3 in Action (подождите второго издания) Ryan Bigg и Yehuda Katz для изучения Rails.
  • PeepCode и Code School: скринкасты от PeepCode и интерактивные курсы в Code School неизменно высокого качества и я горячо рекомендую их.

11.5 Упражнения

  1. Добавьте тесты для уничтожения взаимоотношений связанных с данным пользователем (т.e., к реализации dependent :destroy в Листинге 11.4 и Листинге 11.16). Подсказка: следуйте примеру в Листинге 10.12.
  2. Метод respond_to виденый в Листинге 11.38 на самом деле может быть поднят из действий в сам контроллер Relationships, и respond_to блоки могут быть заменены на Rails метод respond_with. Подтвердите, что результирующий код, показаный в Листинге 11.47, является корректным, проверив что набор тестов все еще проходит. (Подробнее об этом методе см в Google поиске “rails respond_with”.)
  3. Сделайте рефакторинг Листинга 11.31 добавив партиалы для кода общего для страниц following/followers, страницы Home, и страницы показывающей пользователя.
  4. Следуя модели в Листинге 11.19, напишите тесты для статистики на странице профиля.
Листинг 11.47. Компактный рефакторинг Листинга 11.38.
class RelationshipsController < ApplicationController
  before_action :signed_in_user

  respond_to :html, :js

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    respond_with @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    respond_with @user
  end
end
  1. Фотографии для набросков взяты с http://www.flickr.com/photos/john_lustig/2518452221/ и http://www.flickr.com/photos/30775272@N05/2884963755/
  2. Первое издание этой книги использовало терминологию user.following, в которой даже я иногда путался. Благодарю читателя Cosmo Lee за то что он убедил меня изменить терминологию и за сформулированные советы о том как сделать это более понятным. (Однако не последовал его совету в точности, так что если вы все еще путаетесь - это не его вина.) 
  3. Для простоты, Рис. 11.6 подавляет id столбец таблицы following
  4. Более подробно об этом см. when to use let at Stack Overflow
  5. Технически, Rails использует underscore метод для преобразования имени класса в id. Например, "FooBar".underscore является "foo_bar", поэтому внешним ключом для объекта FooBar будет foo_bar_id. (Кстати, инверсией underscore является camelize, который конвертирует camel_case в CamelCase.) 
  6. Если вы заметили что followed_id также идентифицирует пользователя, и обеспокоены ассиметричным обращением с читателями и читаемыми, вы готовы к любым неожиданностям. Мы займемся этим вопросом в Разделе 11.1.5
  7. Если у вас есть большой опыт моделирования конкретной предметной области, вы зачастую можете предугадать такие вспомогательные методы, и даже если нет, вы часто обнаруживаете себя за их написанием с целью почистить тесты. Однако в данном случае нормально если вы не угадали их. Разработка програмного обеспечения это обычно итеративный процесс — вы пишете код до тех пор пока он не начинает становиться уродливым, а затем вы рефакторите его — но, для краткости, изложение в учебнике немного сглажено. 
  8. Так как номинально это является акронимом asynchronous JavaScript and XML, Ajax иногда ошибочно пишут как “AJAX”, хотя на протяжении всей оригинальной Ajax статьи используется написание “Ajax”. 
  9. Это работает только если JavaScript включен в браузере, но изящно деградирует, работая в точности как в Разделе 11.2.4 если JavaScript отключен. 
  10. Основное требование заключается в том, что перечисляемые объекты должны реализовывать each метод для перебора коллекции. 
  11. На самом деле такая нотация на самом деле изначально была расширением которое Rails вносил в ядро ​​языка Ruby; она была настолько полезной, что в настоящее время она включена в сам Ruby. Замечательно, правда? 
  12. Для более продвинутых способов создания необходимых подзапросов, см. сообщение в блоге “Hacking a subselect in ActiveRecord”. 
  13. Для того чтобы сделать поток сообщений на Рис. 11.20 более привлекательным, я добавил несколько дополнительных микросообщений вручную используя Rails консоль. 
  14. Единственная моя оговорка по поводу Railscasts — они обычно опускают тесты. Это, вероятно, необходимо для сохранения красоты и краткости эпизодов, но у вас может сформироваться неправильное представление о важности тестов. После просмотра соответствующего Railscast для получения представления о процессе, я советую писать новую фнкцию используя разработку через тестирование. (В этом контексте я рекомендую взглянуть на the RailsCast on “How I test”. Вы увидите, что Ryan Bates сам обычно использует TDD для разработки в реальной жизни и что, фактически, его стиль тестирования совпадает со стилем используемым в данном учебнике.) 
  15. # позволю себе небольшой комментарий к переводу этой главы. В тексте очень часто употребляются различные формы слова follow (following, follower, followers и т.п.) в дословном переводе это означает "следовать" (слежение, следящий, следящие соответственно). Однако Twitter, (клоном которого является пример приложения, рассматриваемый в этом учебнике) вместо дословного перевода использует термины "Читает" (для following) и "Читают" (для followers) и лично мне термин "Читатели" вместо возможного дословного перевода (следователи, последователи и т.п.) нравится больше. И, несмотря на несколько казусов с этим связанных, в оставшейся части учебника я постараюсь придерживаться именно этого варианта.
    P.s. Надеюсь данный комментарий не запутал вас окончательно. :)