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

Глава 7 Регистрация

Теперь, когда у нас есть рабочая модель User, пришло время добавить возможность, без которой некоторые сайты просто жить не могут: позволить пользователям регистрироваться на сайте. Мы будем использовать HTML форму для предоставления пользователями регистрационной информации нашему приложению в Разделе 7.2, которая затем будет использована для создания нового пользователя и сохранения его атрибутов в базе данных в Разделе 7.4. В конце процесса регистрации важно отобразить страницу профиля с информацией о новом созданном пользователе, так что мы начнем с создания страницы для демонстрации пользователей, которая будет для нас первым шагом на пути реализации REST архитектуры для пользователей (Раздел 2.2.2). Как обычно, мы будем писать тесты по мере разработки, расширяя наши познания о применении RSpec и Capybara для написания кратких и выразительных интеграционных тестов.

Для того чтобы создать страницу профиля пользователя, нам необходимо иметь пользователя в базе данных, что представлеяет проблему яйца и курицы: как сайт может иметь пользователя до появления рабочей страницы регистрации? К счастью, эта проблема уже решена: в Разделе 6.3.5 мы создавали запись User вручную с помощью Rails консоли. Если вы пропустили этот раздел, вам следует вернуться и закончить его перед продолжением.

Если вы используете управление версиями, создайте тему ветки, как обычно:

$ git checkout master
$ git checkout -b sign-up

7.1 Демонстрация пользователей

В этом разделе мы сделаем первый шаг на пути к конечной странице пользователя, создав страницу для отображения имени и фотографии пользователя, на что указывает набросок страницы на Рис. 7.1.1 Нашей конечной целью для страниц профиля пользователя является отображение фотографии пользователя, основных данных пользователя и списка микросообщений, как показано на наброске в Рис. 7.2.2 (на Рис. 7.2 показан наш первый пример lorem ipsum текста, который имеет захватывающую историю которую вам решительно стоит прочитать на досуге.) Мы закончим эту задачу, а вместе с ней и пример приложения, в Главе 11.

profile_mockup_profile_name_bootstrap
Рис. 7.1: Набросок страницы профиля пользователя которая будет сделана в этом разделе. (полный размер)
profile_mockup_bootstrap
Рис. 7.2: Набросок наших лучших ожиданий от конечной страницы профиля. (полный размер)

7.1.1 Отладка и окружения Rails

Профили в этом разделе будут первыми по-настоящему динамическими страницами в нашем приложении. Хотя представление будет существовать как одна страница кода, каждый профиль будет кастомизирован с использованием информации получаемой из базы данных сайта. В качестве подготовки к добавлению динамических страниц в наш пример приложения, сейчас хорошее время, чтобы добавить некоторую отладочную информацию к шаблону нашего сайта (Листинг 7.1). Это отображает некоторую полезную информацию о каждой странице с помощью встроенного debug метода и params переменной (о которой мы узнаем больше в Разделе 7.1.2).

Листинг 7.1. Добавление отладочной информации к шаблону сайта.
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  .
  .
  .
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>
</html>

Для того чтобы вывод отладочной информации приятно выглядел, мы добавим несколько правил к кастомной таблице стилей созданной в Главе 5, как это показано в Листинге 7.2.

Листинг 7.2. Добавление кода для красивого блока с отладочной информацией, включая примесь Sass.
app/assets/stylesheets/custom.css.scss
@import "bootstrap";

/* mixins, variables, etc. */

$grayMediumLight: #eaeaea;

@mixin box_sizing {
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}
.
.
.

/* miscellaneous */

.debug_dump {
  clear: both;
  float: left;
  width: 100%;
  margin-top: 45px;
  @include box_sizing;
}

Этот код вводит новую для нас возможность Sass - примесь, в данном случае названную box_sizing. Примесь позволяет группировать CSS правила с тем чтобы они могли использоваться для нескольких элементов, конвертируя

.debug_dump {
  .
  .
  .
  @include box_sizing;
}

в

.debug_dump {
  .
  .
  .
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}

Мы еще раз применим эту примесь в Разделе 7.2.2. Результат ее применения в контексте блока с отладочной информацией показан на Рис. 7.3.

home_page_with_debug_4_0
Рис. 7.3: Home страница примера приложения (/) с отладочной информацией. (полный размер)

Вывод отладочной информации на Рис. 7.3 дает потенциально полезную информацию об отображаемой в настоящий момент странице:

---
controller: static_pages
action: home

Это YAML3 представление params, который по сути является хэшем и в данном случае указывает на контроллер и действие страницы. Мы увидим еще один пример в Разделе 7.1.2

Поскольку мы не хотим показывать отладочную информацию пользователям развернутого приложения, Листинг 7.1 использует

if Rails.env.development?

для того чтобы показывать отладочную информацию только в среде разработки, которая является одной из трех сред, определенных по умолчанию в Rails (Блок 7.1).4 В частности, Rails.env.development? является true только в среде разработки, так что Embedded Ruby

<%= debug(params) if Rails.env.development? %>

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

7.1.2 Ресурс Users

В конце Главы 6 мы создали нового пользователя в базе данных. Как показано в Разделе 6.3.5, этот пользователь имеет id 1 и наша цель теперь заключается в создании страницы для отображения информации этого пользователя. Мы будем следовать соглашениям REST архитектуры предпочитаемой в Rails приложениях (Блок 2.2), что означает представление данных в качестве ресурсов которые могут быть созданы, показаны, обновлены, или уничтожены — четыре действия, соответствующие четырем фундаментальным операциям POST, GET, PATCH и DELETE определенным стандартом HTTP (Блок 3.3).

Следуя принципам REST, на ресурсы обычно ссылаются, используя имя ресурса и уникальный идентификатор. Что это означает в контексте пользователей — о которых мы теперь думаем, как о ресурсе Users—то, что мы должны показать пользователя с id 1 выдав GET запрос к URL /users/1. Здесь show действие неявно в типе запроса —когда Rails’ функции REST активированы, GET запросы автоматически обрабатываются show действием.

Мы видели в Разделе 2.2.1 что страница для пользователя с id 1 имеет URL /users/1. К сожалению, сейчас посещение этой страницы лишь выдает ошибку (Рис. 7.4).

profile_routing_error
Рис. 7.4: Страница ошибки для /users/1. (полный размер)

Мы можем заполучить REST-style Users URL на работу, добавив одну-единственную строку в наш файл маршрутов (config/routes.rb):

resources :users

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

Листинг 7.3. Добавление ресурса Users в файл маршрутов.
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users
  root  'static_pages#home'
  match '/signup',  to: 'users#new',            via: 'get'
  .
  .
  .
end

Вы возможно заметили, что листинг 7.3 удаляет строку

get "users/new"

последний раз замеченную в Листинге 5.35. Это связано с тем, что resources :users не просто добавляет работающий /users/1 URL; эта строка также обеспечивает наш пример приложения всеми действиями, необходимыми для RESTful (полностью REST) ресурса Users,5 наряду с большим количеством именованных маршрутов (Раздел 5.3.3) для генерации URL пользователя. Получившееся соответствие URL, действий и именованных маршрутов показано в Таблице 7.1. (Сравните с Таблицей 2.2.) В течение следующих трех глав, мы охватим остальные записи в Таблице 7.1 поскольку мы заполним все действия, необходимые, для того, чтобы сделать Users RESTful ресурсом.

HTTP запросURLДействиеИменованный маршрутНазначение
GET/usersindexusers_pathстраница показывающая список всех пользователей
GET/users/1showuser_path(user)страница показывающая пользователя
GET/users/newnewnew_user_pathстраница для создания нового пользователя (регистрация)
POST/userscreateusers_pathсоздание нового пользователя
GET/users/1/editeditedit_user_path(user)страница для редактирования пользователя с id 1
PATCH/users/1updateuser_path(user)обновление пользователя
DELETE/users/1destroyuser_path(user)удаление пользователя
Table 7.1: RESTful маршруты, обеспеченные ресурсом Users в Листинге 7.3.

С кодом в Листинге 7.3, маршруты работают, но страница по-прежнему не существует (Рис. 7.5). Для того чтобы исправить это, мы начнем с минималистичной версии страницы профиля, которую мы откормим в Разделе 7.1.4.

user_show_unknown_action_4
Рис. 7.5: URL /users/1 с маршрутом, но без страницы. (полный размер)

Мы будем использовать стандартное Rails размещение для представления показывающего пользователя, которым является app/views/users/show.html.erb. В отличие от представления new.html.erb, которое мы создали с помощью генератора в Листинге 5.31, файл show.html.erb в данный момент не существует, так что нам необходимо создать его вручную и заполнить его содержимым Листинга 7.4.

Листинг 7.4. Представление-заглушка для отображения информации пользователя.
app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>

Это представление использует Embedded Ruby для отображения имени и адреса электронной почты пользователя, предполагая наличие переменной экземпляра @user. Конечно же, в итоге реальная страница пользователя будет выглядеть совершенно иначе и не будет публично демонстрировать адрес электронной почты.

Для того чтобы заставить представление user show работать, нам необходимо определить переменную @user в соответствующем show действии контроллера Users. Как вы и ожидали, мы используем метод find на модели User (Раздел 6.1.4) для получения пользователя из базы данных, как это показано в Листинге 7.5.

Листинг 7.5. Контроллер Users с действием show action.
app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
  end
end

Здесь мы использовали params для получения id пользователя. Когда мы сделаем соответствующий запрос в контроллер Users, params[:id] будет пользовательским id 1, так что эффект тот же что и с методом find

User.find(1)

который мы видели в Разделе 6.1.4. (Технически, params[:id] является строкой "1", но find достаточно умен, чтобы преобразовать это в целое число.)

С представлением и определенным действием, URL /users/1 работает замечательно (Рис. 7.6). Обратите внимание что отладочная информация в Рис. 7.6 подтверждает значение params[:id]:

---
action: show
controller: users
id: '1'

Вот почему код

User.find(params[:id])

в Листинге 7.5 находит пользователя с id 1.

user_show_bootstrap
Рис. 7.6: Страница показывающая пользователя на /users/1 после добавления ресурса Users. (полный размер)

7.1.3 Тестирование страницы показывающей пользователя (с фабриками)

Теперь, когда у нас есть минимально рабочий профиль, пришло время поработать над версией из наброска на Рис. 7.1. Как и в случае с созданием статических страниц (Глава 3) и моделью User (Глава 6), мы сделаем это используя разработку через тестирование.

Вспомните из Раздела 5.4.2 что для страниц связанных с ресурсом Users мы приняли решение использовать интеграционные тесты. В случае со страницей регистрации, наш тест вначале посещает signup_path а затем проверяет правильность h1 и title тегов, как это показано в Листинге 5.34 и повторено в Листинге 7.6. (Обратите внимание, что мы опустили full_title хелпер из Раздела 5.3.4 поскольку полный заголовок уже адекватно протестирован.)

Листинг 7.6. Резюме начального спека страниц пользователя.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }

  describe "signup page" do
    before { visit signup_path }

    it { should have_content('Sign up') }
    it { should have_title(full_title('Sign up')) }
  end
end

Для того чтобы протестировать страницу показывающую пользователя, нам необходим объект модели User для того чтобы коду в действии show (Листинг 7.5) было что искать:

describe "profile page" do
  # Replace with code to make a user variable
  before { visit user_path(user) }

  it { should have_content(user.name) }
  it { should have_title(user.name) }
end

где нам нужно заполнить коментарий соответствующим кодом. Здесь используется именованный маршрут user_path (Таблица 7.1) для генерации пути к странице показывающей данного пользователя. Затем мы тестируем что и страница и тайтл содержат имя пользователя.

Для того чтобы создать необходимый объект модели User, мы можем использовать Active Record для создания пользователя посредством User.create, но практика показывает, что пользовательские фабрики являются более удобным способом определения и вставки в базу данных объектов user. Мы будем использовать фабрики генерируемые Factory Girl - Ruby гемом который сделан хорошими людьми из thoughtbot. Как и RSpec, Factory Girl определяет предметно-ориентированный язык в Ruby, в данном случае предназначенный для определения объектов Active Record. Синтаксис прост, опирается на Ruby блоки и кастомные методы для определения атрибутов описываемого объекта. Для случаев вроде тех что мы увидим в этой главе, преимущество над Active Record может быть неочевидным, но мы будем использовать более продвинутые техники фабрик в последующих главах. Например, в Разделе 9.3.3 нам понадобится создать набор пользователей с уникальными адресами электронной почты и фабрики позволят нам проделать это с легкостью.

Как и с другими Ruby гемами, мы можем установить Factory Girl добавив строку в Gemfile используемый Bundler-ом (Listing 7.7). (Поскольку Factory Girl нужна только в тестах, мы поместили ее в группу :test.)

Листинг 7.7. Добавление Factory Girl в Gemfile.
source 'https://rubygems.org'
  .
  .
  .
group :test do
  .
  .
  .
  gem 'factory_girl_rails', '4.2.1'
end
.
.
.

Затем устанавливаем как обычно:

$ bundle install

Мы поместим все наши фабрики Factory Girl в файл spec/factories.rb, который автоматически будет загружен RSpec-ом. Код необходимый для создания фабрики User представлен в Листинге 7.8.

Листинг 7.8. Фабрика для симуляции объектов модели User.
spec/factories.rb
FactoryGirl.define do
  factory :user do
    name     "Michael Hartl"
    email    "[email protected]"
    password "foobar"
    password_confirmation "foobar"
  end
end

Передавая символ :user команде factory, мы говорим Factory Girl что последующее определение предназначено для объекта модели User.

С определением в Листинге 7.8, мы можем создать фабрику User в тестах используя команду let (Блок 6.3) и метода FactoryGirl поддерживаемого Factory Girl:

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

Конечный результат представлен в Листинге 7.9.

Листинг 7.9. Тест для страницы показывающей пользователя.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }

  describe "profile page" do
    let(:user) { FactoryGirl.create(:user) }
    before { visit user_path(user) }

    it { should have_content(user.name) }
    it { should have_title(user.name) }
  end

  describe "signup page" do
    before { visit signup_path }

    it { should have_content('Sign up') }
    it { should have_title(full_title('Sign up')) }
  end
end

В этой точке вам необходимо проверить что набор тестов в красном:

$ bundle exec rspec spec/

Мы можем получить зеленые тесты с кодом из Листинга 7.10.

Листинг 7.10. Добавление заголовка браузера и заголовка первого уровня для страницы профиля пользователя.
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<h1><%= @user.name %></h1>

Повторный запуск тестов должен подтвердить что тесты из Листинга 7.9 теперь проходят:

$ bundle exec rspec spec/

Вы быстро заметите что тесты с Factory Girl - медленные. И это происходит не по вине Factory Girl и фактически это фича, а не баг. Проблема связана с тем, что алгоритм BCrypt используемый в Разделе 6.3.1 для создания хэша безопасного пароля является медленным по своей природе: именно низкая скорость BCrypt отчасти делает его таким сложным для взлома. К сожалению, это означает что создание пользователей может утопить набор тестов; к счастью, есть простой способ исправить это. Библиотека bcrypt-ruby использует фактор стоимости для управления вычислительной сложностью создаваемого безопасного хэша. Дефолтное значение призвано обеспечить безопасность, а не скорость и для рабочих приложений это здорово, но в тестах наши потребности противоположны: мы хотим быстрые тесты и нас совершенно не тревожит безопасность хэшей паролей тестовых пользователей. Решением будет добавление строки которая переопределит фактор стоимости от дефолтного безопасного значения к минимально возможному в файл конфигурации тестового окружения config/environments/test.rb, как это показано в Листинге 7.11. Даже для небольшого набора тестов выигрыш в скорости от этого шага может быть весьма значительным и я настоятельно рекомендую включить Листинг 7.11 в ваш test.rb конфиг.

Листинг 7.11. Переопределение фактора стоимости Bсrypt в тестовом окружении.
config/environments/test.rb
SampleApp::Application.configure do
  .
  .
  .
  # Speed up tests by lowering bcrypt's cost function.
  ActiveModel::SecurePassword.min_cost = true
end

7.1.4 Изображение Gravatar и боковая панель

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

Мы начнем с добавления “глобально распознаваемого аватара” (также известного как Граватар) к профилю пользователя.6 Автором Граватар является Tom Preston-Werner (сооснователь GitHub), впоследствии его (Граватар) приобрела компания Automattic (создатели WordPress), это бесплатный сервис который позволяет пользователям загружать изображения и связывать их с подконтрольными им адресами электронной почты. Gravatar это удобный способ добавить изображения пользователей не связываясь с проблемами загрузки изображений, их обрезкой и хранением; все что нам нужно, это создать правильный URL Gravatar изображения используя адрес электронной почты пользователя и соответствующее изображение Gravatar появится автоматически.7

Мы планируем определить вспомогательную функцию gravatar_for которая будет возвращать изображение Граватар для данного пользователя, как это показано в Листинге 7.12.

Листинг 7.12. Представление показывающее пользователя с именем и Граватаром.
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<h1>
  <%= gravatar_for @user %>
  <%= @user.name %>
</h1>

В этой точке вы можете проверить что набор тестов не проходит:

$ bundle exec rspec spec/

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

По умолчанию, методы определеннные в любом файле хелпера является автоматически доступным в любом представлении, но для удобства мы поместим метод gravatar_for в файл для хелперов, связанных с контроллером Users. Как отмечено на главной странице Граватар, URL Граватара основывается на MD5 хэше адреса электронной почты пользователя. В Ruby, алгоритм MD5 хэширования реализутся с помощью метода hexdigest, который является частью библиотеки Digest:

>> email = "[email protected]".
>> Digest::MD5::hexdigest(email.downcase)
=> "1fda4469bcbec3badf5418269ffc5968" 

Поскольку адреса электронной почты нечувствительны к регистру (Раздел 6.2.4), в отличие от MD5 хэшей, мы используем метод downcase для того чтобы быть уверенными в том, что аргумент передаваемый в hexdigest находится в нижнем регистре. Получившийся хелпер gravatar_for представлен в Листинге 7.13.

Листинг 7.13. Определение вспомогательного метода gravatar_for.
app/helpers/users_helper.rb
module UsersHelper

  # Returns the Gravatar (http://gravatar.com/) for the given user.
  def gravatar_for(user)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

Код в Листинге 7.13 возвращает тег img для Граватара с классом "gravatar" и альтернативным текстом эквивалентным имени пользователя (что особенно удобно для браузеров используемых слабовидящими людьми которые работают с помощью считывателей экрана). Вы можете проверить что теперь набор тестов проходит:

$ bundle exec rspec spec/

Страница профиля представленная на Рис. 7.7, показывающая дефолтное изображение Граватара, выглядит подобным образом т.к. [email protected] это невалидный email адрес (домен example.com зарезервирован для примеров (examples).

profile_with_gravatar_bootstrap_4_0
Рис. 7.7: Страница профиля пользователя /users/1 с дефолтным Граватаром. (полный размер)

Для того чтобы отобразить кастомный Граватар в нашем приложении, мы будем использовать update_attributes (Раздел 6.1.5) для обновления пользователя в базе данных:

$ rails console
>> user = User.first
>> user.update_attributes(name: "Example User",
?>                        email: "[email protected]",
?>                        password: "foobar",
?>                        password_confirmation: "foobar")
=> true

Здесь мы назначили пользователю адрес электронной почты [email protected], который я связал с логотипом Rails Tutorial, как это видно на Рис. 7.8.

profile_custom_gravatar_bootstrap_4_0
Рис. 7.8: Страница показывающая пользователя с кастомным Граватаром. (полный размер)

Последний элемент, необходимый для завершения наброска из Рис. 7.1 это начальная версия пользовательской боковой панели. Мы реализуем ее с помощью тега aside, который используется для элементов (таких как сайдбары), которые дополняют страницу, но также могут быть использованы отдельно. Мы включаем row и span4 классы, которые являются частью Bootstrap. Код для измененной страницы показывающей пользователя представлен в Листинге 7.14.

Листинг 7.14. Добавление боковой панели к представлению user show.
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>
  </aside>
</div>

Имея соответствующие HTML элементы и CSS классы, мы можем отстилить страницу профиля (включая боковую панель и Gravatar) с SCSS показанным в Листинге 7.15. (Обратите внимание на наследование CSS правил, которое работает только благодаря препроцессору Sass, используемому файлопроводом.) Получившаяся страница показана на Рис. 7.9.

Листинг 7.15. SCSS для стилизации страницы показывающей пользователя, включая боковую панель.
app/assets/stylesheets/custom.css.scss
.
.
.

/* sidebar */

aside {
  section {
    padding: 10px 0;
    border-top: 1px solid $grayLighter;
    &:first-child {
      border: 0;
      padding-top: 0;
    }
    span {
      display: block;
      margin-bottom: 3px;
      line-height: 1;
    }
    h1 {
      font-size: 1.4em;
      text-align: left;
      letter-spacing: -1px;
      margin-bottom: 3px;
      margin-top: 0px;
    }
  }
}

.gravatar {
  float: left;
  margin-right: 10px;
}
user_show_sidebar_css_bootstrap
Рис. 7.9: Страница показывающая пользователя /users/1 с боковой панелью и CSS. (полный размер)

7.2 Форма регистрации

Теперь, когда у нас есть рабочая (хоть и незавершенная) страница профиля пользователя, мы готовы приступить к созданию формы для регистрации на нашем сайте. Мы видели на Рис. 5.9 (дублированном на Рис. 7.10) что страница регистрации в настоящий момент пуста и совершенно бесполезна для регистрации новых пользователей. Целью этого раздела является разработка формы регистрации из Рис. 7.11.

new_signup_page_bootstrap
Рис. 7.10: Текущее состояние страницы регистрации /signup(полный размер)
signup_mockup_bootstrap
Рис. 7.11: Набросок страницы регистрации пользователей. (полный размер)

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

$ bundle exec rake db:reset

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

$ bundle exec rake test:prepare

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

7.2.1 Тесты для регистрации пользователя

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

Мы уже видели как Capybara поддерживает интуитивно понятный синтаксис веб-навигации. До сих пор мы в основном использовали visit для посещения конкретных страниц, но Capybara может гораздо больше, включая заполнение полей, вроде тех что мы видели на Рис. 7.11 и кликанья по кнопке. Синтаксис выглядит примерно так:

visit signup_path
fill_in "Name", with: "Example User"
.
.
.
click_button "Create my account"

Сейчас нашей целью является написание тестов для правильного поведения при предоставлении невалидной и валидной регистрационной информации. Поскольку эти тесты довольно продвинутые, мы будем писать их постепенно. Если вам интересно посмотреть как они работают (включая файл в который они должны быть помещены), вы можете перейти непосредственно к Листингу 7.16.

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

visit signup_path
click_button "Create my account"

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

visit signup_path
fill_in "Name",         with: "Example User"
fill_in "Email",        with: "[email protected]"
fill_in "Password",     with: "foobar"
fill_in "Confirmation", with: "foobar"
click_button "Create my account"

Эти тесты проверяют что поведение приложения после клика по кнопке “Create my account” соответствует нашим ожиданиям: создается новый пользователь если информация валидна и не создается новый пользователь в случае когда она невалидна. Для того чтобы сделать это мы проверяем количество пользователей и наши тесты будут использовать метод count, доступный для каждого класса Active Record, включая User:

$ rails console
>> User.count
=> 0

Здесь User.count является 0 поскольку мы очистили базу данных в начале этого раздела.

При отправке невалидных данных, мы ожидаем что количество пользователей не будет изменено; при отправке валидных данных мы ожидаем что оно изменится на 1. Мы можем выразить это в RSpec скомбинировав метод expect с методом to или методом not_to. Мы начнем со случая с невалидными данными поскольку он проще; мы посетим страницу регистрации и кликнем по кнопке, и мы ожидаем что это не изменит количества пользователей:

visit signup_path
expect { click_button "Create my account" }.not_to change(User, :count)

Обратите внимание: expect обворачивает click_button в блок (Раздел 4.3.2). Что необходимо для работы метода change, который принимает в качестве аргумента объект и символ, а затем вычисляет результат вызова этого символа в качестве метода на объекте до и после блока. Другими словами, код

expect { click_button "Create my account" }.not_to change(User, :count)

вычисляет

User.count

до и после выполнения

click_button "Create my account"

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

initial = User.count
click_button "Create my account"
final = User.count
expect(initial).to eq final

на одну строку

expect { click_button "Create my account" }.not_to change(User, :count)

которая читается как естественный язык и является намного более компактной (eq это RSpec метод для тестирования на равенство).

Случай с валидными данными аналогичен, но вместо проверки того, что количество пользователей не изменилось, мы проверяем что клик по кнопке изменяет их количество на 1:

visit signup_path
fill_in "Name",         with: "Example User"
fill_in "Email",        with: "[email protected]"
fill_in "Password",     with: "foobar"
fill_in "Confirmation", with: "foobar"
expect do
  click_button "Create my account"
end.to change(User, :count).by(1)

Здесь используется метод to поскольку мы ожидаем что клик по кнопке (вкупе с валидными данными) изменит количество пользователей на единицу.

Комбинирование двух случаев с соответствующими блоками describe и выталкивание общего кода в блоки before, приводит нас к хорошим базовым тестам для регистрации пользователей, как это показано в Листинге 7.16. Здесь мы вынесли общий текст для кнопки регистрации в переменную submit, которую мы определили с помощью метода let.

Листинг 7.16. Хорошие базовые тесты для регистрации пользователей.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }
  .
  .
  .
  describe "signup page" do

    before { visit signup_path }

    let(:submit) { "Create my account" }

    describe "with invalid information" do
      it "should not create a user" do
        expect { click_button submit }.not_to change(User, :count)
      end
    end

    describe "with valid information" do
      before do
        fill_in "Name",         with: "Example User"
        fill_in "Email",        with: "[email protected]"
        fill_in "Password",     with: "foobar"
        fill_in "Confirmation", with: "foobar"
      end

      it "should create a user" do
        expect { click_button submit }.to change(User, :count).by(1)
      end
    end
  end
end

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

Конечно же, в этой точке тесты должны быть провальными:

$ bundle exec rspec spec/

7.2.2 Применение form_for

Теперь, когда у нас есть хорошие провальные тесты для регистрации пользователя, мы начнем добиваться их прохождения, для начала создав форму для регистрации пользователей. Мы можем сделать это в Rails с помощью вспомогательного метода form_for который принимает объект Active Record и конструирует форму используя атрибуты объекта. Результат представлен в Листинге 7.17. (Читатели знакомые с Rails 2.x должны обратить внимание что form_for использует “процент-равно” ERb синтаксис для вставки контента; там где Rails 2.x использовали <% form_for ... %>, мы теперь используем <%= form_for ... %>.)

Листинг 7.17. Форма для регистрации новых пользователей.
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(@user) do |f| %>

      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.text_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Create my account", class: "btn btn-large btn-primary" %>
    <% end %>
  </div>
</div>

Давайте рассмотрим этот код по частям. Наличие ключевого слова do указывает на то что form_for принимает блок с одной переменной, которую мы назвали f (от “form”):

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

Как это зачастую происходит с хелперами Rails, нам нет надобности знать о подробностях реализации, но нам необходимо знать что делает объект f: при вызове с методом соответствующим элементу HTML формы — таким как текстовое поле, радио кнопка, или поле пароля — он возвращает особым образом организованный код для этого элемента для установки атрибута объекта @user. Другими словами,

<%= f.label :name %>
<%= f.text_field :name %>

создает HTML необходимый для создания элемента маркированного текстового поля для назначения атрибута name модели User. (Мы взглянем на сам HTML в Разделе 7.2.3.)

Для того чтобы увидеть это в действии нам необходимо посмотреть на HTML производимый этой формой, но здесь мы сталкиваемся с проблемой: страница в настоящее время сломана, поскольку мы не установили переменную @user — как все неопределенные переменные экземпляра (Раздел 4.4.5), @user в настоящее время является nil. Соответственно, если вы запустите ваш набор тестов в этой точке, вы увидите что тесты для содержимого страницы регистрации из Листинга 7.6 сейчас не проходят:

$ bundle exec rspec spec/requests/user_pages_spec.rb -e "signup page"

(Флаг -e в данном случае позволяет запустить только спеки, чье описание совпадает с "signup page". В частности, обратите внимание что это не подстрока "signup", что привело бы к запуску всех тестов в Листинге 7.16.) Для того чтобы вновь сделать эти тесты проходящими и для того чтобы отрендерить нашу форму, мы должны определить переменную @user в действии контроллера, соответствующем new.html.erb, т.е., в действии new контроллера Users. Хелпер form_for ожидает что @user будет объектом User и поскольку мы создаем нового пользователя, мы просто используем User.new, как это видно в Листинге 7.18.

Листинг 7.18. Добавление переменной @user в действие new.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def new
    @user = User.new
  end
end

После определения переменной @user, тесты для страницы регистрации должны пройти:

$ bundle exec rspec spec/requests/user_pages_spec.rb -e "signup page"

В этой точке у нас должна получиться форма (со стилями из Листинга 7.19) выглядящая как на Рис. 7.12. Обратите внимание на повторное использование примеси box_sizing из Листинга 7.2.

Листинг 7.19. CSS для формы регистрации.
app/assets/stylesheets/custom.css.scss
.
.
.

/* forms */

input, textarea, select, .uneditable-input {
  border: 1px solid #bbb;
  width: 100%;
  margin-bottom: 15px;
  @include box_sizing;
}

input {
  height: auto !important;
}
signup_form_bootstrap
Рис. 7.12: Форма регистрации новых пользователей /signup(полный размер)

7.2.3 HTML формы

Как нам подсказывает Рис. 7.12, теперь страница регистрации рендерится как следует, что указывает на то, что код form_for в Листинге 7.17 производит валидный HTML. Если вы взглянете на HTML для сгенерированной формы (используя Firebug или функцию “просмотр исходного кода страницы” вашего браузера), вы должны увидеть разметку как в Листинге 7.20. Хотя большая часть деталей несущественна для наших целей, давайте улучим минутку и ознакомимся с наиболее важными частями ее структуры.

Листинг 7.20. HTML для формы на Рис. 7.12.
<form accept-charset="UTF-8" action="/users" class="new_user"
      id="new_user" method="post">

  <label for="user_name">Name</label>
  <input id="user_name" name="user[name]" type="text" />

  <label for="user_email">Email</label>
  <input id="user_email" name="user[email]" type="text" />

  <label for="user_password">Password</label>
  <input id="user_password" name="user[password]"
         type="password" />

  <label for="user_password_confirmation">Confirmation</label>
  <input id="user_password_confirmation"
         name="user[password_confirmation]" type="password" />

  <input class="btn btn-large btn-primary" name="commit" type="submit"
         value="Create my account" />
</form>

(Здесь я опустил HTML связанный с authenticity token, который Rails автоматически включают для предотвращения определенного вида атак, назывемого подделка межсайтовых запросов (CSRF). См введение в Rails authenticity token на Stack Overflow если вас интересуют подробности того как это работает и почему это так важно.)

Мы начнем с внутренней структуры документа. Сравнивая Листинг 7.17 с Листингом 7.20, мы видим что Embedded Ruby

<%= f.label :name %>
<%= f.text_field :name %>

производит HTML

<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />

и

<%= f.label :password %>
<%= f.password_field :password %>

производит HTML

<label for="user_password">Password</label><br />
<input id="user_password" name="user[password]" type="password" />

Как видно из Рис. 7.13, текстовые поля (type="text") просто отображают их содержимое, в то время как поля паролей (type="password") скрывают вводимое в целях безопасности, как это показано на Рис. 7.13.

filled_in_form_bootstrap
Рис. 7.13: Заполненная форма с text и password полями. (полный размер)

Как мы увидим в Разделе 7.4, ключом к созданию пользователя является специальный name атрибут в каждом input:

<input id="user_name" name="user[name]" - - - />
.
.
.
<input id="user_password" name="user[password]" - - - />

Эти значения name позволяют Rails сконструировать хэш инициализации (через переменную params) для создания пользователей с использованием значений введеных пользователем, как мы это увидим в Разделе 7.3.

Второй важный элемент это сам тег form. Rails создает тег form используя объект @user: поскольку каждый объект в Ruby знает свой класс (Раздел 4.4.1), Rails определяет что @user принадлежит к классу User; кроме того, поскольку @user это новый пользователь, Rails знает что необходимо построить форму с post методом, который является правильным глаголом для создания нового объекта (Блок 3.3):

<form action="/users" class="new_user" id="new_user" method="post">

Здесь атрибуты class и id, по большому счету, не имеют особого значения; более важными являются action="/users" и method="post". Совместно они формируют инструкцию для отправки HTTP запроса POST на URL /users. В следующих двух разделах мы увидим к чему это приводит.

7.3 Провальная регистрация

Мы кратко рассмотрели HTML формы показанной на Рис. 7.12 (Листинг 7.20), теперь мы копнем эту тему чуть глубже. Так как HTML формы лучше всего понимается в контексте сбоя регистрации, в этом разделе мы создадим регистрационную форму которая принимает невалидные данные и вновь рендерит страницу регистрации со списком ошибок, как это показано на наброске Рис. 7.14.

signup_failure_mockup_bootstrap
Рис. 7.14: Набросок страницы неудавшейся регистрации. (полный размер)

7.3.1 Рабочая форма

Вспомните из Раздела 7.1.2 что добавление resources :users в файл routes.rb (Листинг 7.3) автоматически обеспечивает наше Rails приложение возможностью отвечать на RESTful URL из Таблицы 7.1. В частности, это приводит к тому, что POST запрос к /users обрабатывается действием create. Наша стратегия для действия create зaключается в использовании отправки формы для создания объекта нового пользователя с помощью User.new, попытке (провальной) сохранить этого пользователя и последующем рендеринге страницы регистрации для возможной повторной отправки формы. Давайте начнем с того что еще раз взглянем на код для формы регистрации:

<form action="/users" class="new_user" id="new_user" method="post">

Как было отмечено в Разделе 7.2.3, этот HTML выдает POST запрос к /users URL.

Нашим первым шагом на пути к зеленым тестам для невалидных данных из Листинга 7.16 будет добавление кода из Листинга 7.21. Этот листинг включает второе использование метода render который мы впервые видели в контексте партиалов (Раздел 5.1.3); как вы можете видеть, render также работает и в действиях контроллера. Обратите внимание что мы воспользовались этой возможностью чтобы представить ветвящуюся структуру if-else, которая позволяет нам обрабатывать случаи сбоя и успеха раздельно, в зависимости от значения @user.save, которое (как мы видели в Разделе 6.1.3) может быть либо true либо false в зависимости от успешности сохранения.

Листинг 7.21. Действие create которое может обрабатывать провальную регистрацию (но не успешную).
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(params[:user])    # Not the final implementation!
    if @user.save
      # Handle a successful save.
    else
      render 'new'
    end
  end
end

Обратите внимание на комментарий: это не конечная реализация, но этого достаточно для начала. Мы закончим реализацию в Разделе 7.3.2.

Лучший способ понять как работает код в Листинге 7.21 это отправить форму с какими-нибудь невалидными регистрационными данными; результат представлен на Рис. 7.15 и полная отладочная информация (с увеличенным размером шрифта) представлена на Рис. 7.16.

signup_failure_rails_4
Рис. 7.15: Сбой регистрации. (полный размер)
signup_failure_rails_4_debug
Рис. 7.16: Отладочная информация сбоя регистрации. (полный размер)

Чтобы получить более четкую картину того как Rails обрабатывает отправку регистрационных данных, давайте поближе познакомимся с user частью хэша параметров из отладочной информации (Рис. 7.16):

"user" => { "name" => "Foo Bar",
            "email" => "foo@invalid",
            "password" => "[FILTERED]",
            "password_confirmation" => "[FILTERED]"
          }

Этот хэш передается в контроллер Users в качестве params и мы видели, начиная с Раздела 7.1.2, что хэш params содержит информацию о каждом запросе. В случае URL типа /users/1, значение params[:id] это id соответствующего пользователя (1 в этом примере). В случае отправки регистрационной формы, params вместо этого содержит хэш хэшей, конструкцию, которую мы впервые видели в Разделе 4.3.3, который представил стратегически названную params переменную в консольной сессии. Эта отладочная информация показывает, что предоставление (отправка) формы дает в результате user хэш с атрибутами, соответствующими предоставленным значениям, где ключи происходят от name атрибутов тегов input которые мы видели в Листинге 7.17; например, значение

<input id="user_email" name="user[email]" type="text" />

с именем "user[email]" это именно email атрибут хэша user.

Хотя хэш-ключи показаны в отладочном выводе в виде строк, в контроллер Users они передаются в виде символов, так что params[:user] на самом деле является хэшем атрибутов пользователя, именно тех атрибутов, что необходимы в качестве аргумента для User.new, как мы впервые видели в Разделе 4.4.5 и как представлено в Листинге 7.21. Это означает, что строка

@user = User.new(params[:user])

практически эквивалентна

@user = User.new(name: "Foo Bar", email: "foo@invalid",
                 password: "foo", password_confirmation: "bar")

В предыдущих версиях Rails использование

@user = User.new(params[:user])

фактически работало, но было по умолчанию небезопасным, требовало особой тщательности при осуществлении довольно глючной процедуры имеющей своей целью защитить базу данных приложения от зловредных пользователей. В Rails 4.0 этот код вызывает ошибку (как показано на Рис. 7.15 и Рис. 7.16 выше), что означает что он безопасен по умолчанию. Мы можем еще раз убедиться в этом проверив что соответствующие тесты не проходят:

$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with invalid information"

7.3.2 Строгие параметры

В Разделе 4.4.5 мы коротко упомянули об идее массового назначения, которая подразумевает инициализацию Ruby-переменной с хэшем значений:

@user = User.new(params[:user])    # Not the final implementation!

Комментарий включенный в Листинг 7.21 и воспроизведенный выше указывает на то что это не финальная реализация. Причина по которой иниацилизация всего хэша params является очень опасной затеей заключается в том что это приводит к передаче в User.new всех данных отправленных пользователем. В частности, предположим что, в дополнение к текущим атрибутам, модель User включает в себя атрибут admin используемый для идентификации пользователей-администраторов сайта. (Мы реализуем именно такой атрибут в Разделе 9.4.1.) Способом присвоить этому атрибуту значение true является передача значения admin=’1’ как части params[:user], этого легко достичь с помощью такого консольного HTTP клиента как curl. Таким образом, передавая весь хэш params в User.new, мы тем самым позволяем любому пользователю сайта получить права администратора включив admin=’1’ в веб-запрос.

Для решения этой проблемы предыдущие версии Rails использовали метод attr_accessible на уровне модели, но в Rails 4.0 более предпочтительной является техника с использованием так называемых строгих параметров на уровне контроллера. Это позваляет нам прописать какие именно параметры являются обязательными, а какие разрешенными. К тому же, передача чистого хэша params (как это было показано выше) приведет к ошибке, т.е. теперь Rails приложения невосприимчивы к уязвимостям массового назначиния по умолчанию.

В данном случае мы хотим требовать от хэша params наличие атрибута :user и мы хотим позволить наличие атрибутов name, email, password и password_confirmation (но только их). Мы можем достигнуть этого следующим образом:

params.require(:user).permit(:name, :email, :password, :password_confirmation)

Этот код вернет версию хэша params содержащего только разрешенные атрибуты (при этом будет вызвана ошибка если отсутствует атрибут :user).

Для облегчения использования этих параметров, обычно вводят вспомогательный метод с названием user_params который возвращает соответствующий инициализационных хэш используемый вместо params[:user]:

@user = User.new(user_params)

Поскольку user_params будет использоваться только внутри контроллера Users и нет никакой надобности открывать к нему доступ внешним пользователям через веб, мы сделаем его приватным используя ключевое слово private, как это показано в Листинге 7.22. (Мы более детально обсудим private в Разделе 8.2.1.)

Листинг 7.22. Использование строгих параметров в действии create.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      # Handle a successful save.
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

С кодом из Листинга 7.22 тесты отправки невалидных данных должны пройти:

$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with invalid information"

7.3.2 Сообщения об ошибках при регистрации

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

$ rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?>                 password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]

Здесь объект errors.full_messages (который мы видели кратко в Разделе 6.2.2) содержит массив сообщений об ошибках.

Как и в консольной сессии выше, сбой сохранения в Листинге 7.21 генерирует список сообщений об ошибках, связанных с объектом @user. Для отображения сообщения в браузере, мы рендерим партиал error_messages на странице user new как это показано в Листинге 7.23. (Написание теста на сообщения об ошибках это хорошая идея и мы оставим эти тесты в качестве упражнения; см. Раздел 7.6.) Стоит отметить что этот партиал сообщений об ошибках лишь первая попытка; конечная версия представлена в Разделе 10.3.2.

Листинг 7.23. Код для отображения сообщений об ошибках в форме регистрации.
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages' %>
      .
      .
      .
    <% end %>
  </div>
</div>

Заметим здесь, что мы render партиал ’shared/error_messages’; что отражает общую конвенцию Rails, которая предписывает размещение частичных шаблонов которые мы планируем использовать во многих контроллерах в специально отведенном каталоге shared/. Это означает, что мы должны создать этот новый каталог вместе с файлом партиала _error_messages.html.erb. Сам партиал представлен в Листинге 7.24.

Листинг 7.24. Партиал для отображения сообщений об ошибках отправки формы регистрации.
app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-error">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li>* <%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

Этот частичный шаблон вводит несколько новых Rails и Ruby конструкций, в том числе два метода для объекта ошибок Rails. Первый метод это count, который просто возвращает количество ошибок:

>> user.errors.count
=> 2

Другой новый метод это any?, который (совместно с empty?) является одним из пары взаимодополняющих методов:

>> user.errors.empty?
=> false
>> user.errors.any?
=> true

Мы видим здесь что метод empty?, который мы впервые увидели в Разделе 4.2.3 в контексте строк, также работает на объекте ошибок Rails, возваращая true для пустого объекта и false в противном случае. Метод any? это просто противоположность empty?, возвращающая true если существует какой-нибудь элемент и false в противном случае. (Кстати, все эти методы — count, empty? и any? — работают и на массивах Ruby. Мы найдем хорошее применение этому факту в Разделе 10.2.)

Другой новой идеей является текстовый хелпер pluralize. По умолчанию, он недоступен в консоли, но мы можем явно его включить через модуль ActionView::Helpers::TextHelper:9

>> include ActionView::Helpers::TextHelper
>> pluralize(1, "error")
=> "1 error"
>> pluralize(5, "error")
=> "5 errors"

Мы видим здесь, что pluralize принимает целочисленный аргумент и возвращает число с правильной версией множественного числа его второго аргумента. В основе этого метода лежит мощный инфлектор, который знает как преобразовать во множественное число огромное количество слов (в том числе, многие с неправильным множественным числом):

>> pluralize(2, "woman")
=> "2 women"
>> pluralize(3, "erratum")
=> "3 errata"

В результате код

<%= pluralize(@user.errors.count, "error") %>

возвращает "0 errors", "1 error", "2 errors" и т.д., в зависимости от количества ошибок, тем самым позволяя нам избежать грамматически неверных фраз вроде "1 errors".

Обратите внимание: Листинг 7.24 включает CSS id error_explanation для использования в стилизации сообщений об ошибках. (Напомним из Раздела 5.1.2 что CSS использует знак решетки # для стилизации id.) Кроме того, Rails автоматически помещает поля с ошибками в divы с CSS классом field_with_errors. Эти метки затем позволят нам отредактироваь стиль сообщений об ошибках с SCSS показанным в Листинге 7.25, который использует Sass функцию @extend для включения функциональности двух классов Bootstrap control-group и error. В результате чего, при провальной регистрации сообщения об ошибках окружены красным как это показано на Рис. 7.17. Поскольку сообщения генерируются валидациями модели, они автоматически изменятся, если вы когда-нибудь поменяете свое мнение о, скажем, формате адресов электронной почты или минимальной длине паролей.

Листинг 7.24. CSS для стилизации сообщений об ошибках.
app/assets/stylesheets/custom.css.scss
.
.
.

/* forms */
.
.
.
#error_explanation {
  color: #f00;
  ul {
    list-style: none;
    margin: 0 0 18px 0;
  }
}

.field_with_errors {
  @extend .control-group;
  @extend .error;
}
signup_error_messages_bootstrap
Рис. 7.17: Сбой регистрации с сообщениями об ошибках. (полный размер)

Для того чтобы увидеть результаты нашей работы в этом разделе, мы повторим шаги из теста провальной регистрации Листинга 7.16 посетив страницу регистрации и кликнув по “Sign up” с пустыми полями ввода. Результат показан на Рис. 7.18. Как вы догадываетесь, глядя на рабочую страницу, в этой точке соответствующий тест тоже должен пройти:

$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with invalid information"
blank_signup_password_digest_bootstrap_4_0
Рис. 7.18: Результат посещения /signup и клика по “Create my account”. (полный размер)

7.4 Успешная регистрация

Получив обработку отправки невалидной формы, теперь пришло время для завершения регистрационной формы, на самом деле сохранив нового пользователя (если валидный) в базу данных. Во-первых, мы попробуем сохранить пользователя; если сохранение пройдет успешно, информация пользователя будет записана в базу данных автоматически, а затем мы перенаправим браузер на страницу профиля пользователя (с дружеским приветствием), как это показано на наброске Рис. 7.19. Если это не удастся, мы просто отступим к сценарию, разработанному в Разделе 7.3.

signup_success_mockup_bootstrap
Рис. 7.19: Набросок успешной регистрации. (полный размер)

7.4.1 Завершенная форма регистрации

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

$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with valid information"

Это происходит из-за того что дефолтным поведением для Rails-действий является рендеринг соответствующего представления, но здесь нет (и не должно быть) представления соответствующего действию create. Вместо этого мы должны перенаправить на другую страницу и будет вполне логично если этой страницей будет профиль вновь созданного пользователя. Тестирование того что рендерится правильная страница оставлено в качестве упражнения (Раздел 7.6); код представлен в Листинге 7.26.

Листинг 7.26. Действие user create с сохранением и переадресацией.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

Обратите внимание на то что мы можем опустить user_path в редиректе, написав просто redirect_to @user для перенаправления на страницу показывающую пользователя.

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

$ bundle exec rspec spec/

7.4.2 Флэш

Перед отправкой валидной регистрации в браузер мы собираемся немного отполировать ее, в соответствии с общепринятой в веб приложениях идеей: добавив сообщение, которое временно появляется на следующей странице (в данном случае, приветствуя нашего нового пользователя), а затем исчезает либо при посещении следующей страницы либо при перезагрузке текущей. Rails-способ сделать это состоит в использовании специальной переменной flash с которой мы можем обращаться как с обычным хэшем. Вы можете даже вспомнить консольный пример из Разделе 4.3.3, где мы видели как для перебора хэша использовался стратегически именованный хэш flash:

$ rails console
>> flash = { success: "It worked!", error: "It failed." }
=> {:success=>"It worked!", error: "It failed."}
>> flash.each do |key, value|
?>   puts "#{key}"
?>   puts "#{value}"
>> end
success
It worked!
error
It failed.

Мы можем организовать отображение содержимого флэш повсеместно на сайте, включив его в шаблон нашего приложения, как это показно в Листинге 7.27. (Этот код представляет из себя особенно уродливую комбинацию HTML и ERb; упражнение в Разделе 7.6 показывает как сделать его более красивым.)

Листинг 7.27. Добавление содержимого переменной flash в шаблон сайта.
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  .
  .
  .
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <% flash.each do |key, value| %>
        <div class="alert alert-<%= key %>"><%= value %></div>
      <% end %>
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
    .
    .
    .
  </body>
</html>

Код в Листинге 7.27 организует вставку каждого флэш элемента в div тег, с CSS классом, указывающим на тип сообщения. Например, если flash[:success] = "Welcome to the Sample App!", то код

<% flash.each do |key, value| %>
  <div class="alert alert-<%= key %>"><%= value %></div>
<% end %>

произведет такой HTML:

<div class="alert alert-success">Welcome to the Sample App!</div>

(Обратите внимание: ключ :success является символом, но встроенный Ruby автоматически конвертирует его в строку "success" перед вставкой в шаблон.) Причина, по которой мы перебираем все возможные пары ключ/значение заключается в том, что благодаря этому мы сможем включать другие виды флэш сообщений. Например, в Разделе 8.1.5 мы увидим flash[:error] используемое для индикации неудачной попытки входа на сайт.10

Написание теста на правильное флэш сообщение оставлено в качестве упражнения (Раздел 7.6) и мы можем получить прохождение теста назначив flash[:success] приветственное сообщение в действии create как это показано в Листинге 7.28.

Листинг 7.28. Добавление флеш сообщения к успешной регистрации пользователя.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

7.4.3 Первая регистрация

Вы можете увидеть результат всей этой работы, зарегистрировав нашего первого пользователя под именем “Rails Tutorial” и с адресом электронной почты “[email protected]”. Получившаяся в результате страница (Рис. 7.20) показывает дружеское сообщение после успешной регистрации, включая приятный зеленый стиль для класса success, который был добавлен CSS фреймворком Bootstrap из Раздела 5.1.2. (Если вместо этого вы получили сообщение об ошибке указывающее на то что адрес электронной почты уже занят, убедитесь что вы выполнили db:reset Rake-задачу как это предлагалось сделать в Разделе 7.2.) Затем, после перезагрузки страницы профиля пользователя, флэш сообщение исчезает, как и было обещано (Рис. 7.21).

signup_flash_bootstrap
Рис. 7.20: Результирующая страница успешной регистрации с флэш сообщением. (полный размер)
signup_flash_reloaded_bootstrap
Рис. 7.21: Профиль пользователя с исчезнувшим после перезагрузки страницы флэш сообщением. (полный размер)

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

$ rails console
>> User.find_by(email: "[email protected]")
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]",
created_at: "2013-03-12 05:51:34", updated_at: "2013-03-12 05:51:34",
password_digest: "$2a$10$A58/j7wwh3aAffGkMAO9Q.jjh3jshd.6akhDKtchAz/R...">

7.4.4 Развертывание приложения на сервере с SSL

Мы разработали модель User и функционал регистрации, пришло время развернуть пример приложения на сервере. (Если вы еще не выполнили шаги из введения к Главе 3, вам следует вернуться и сделать это сейчас.) В рамках развертывания мы добавим Secure Sockets Layer (SSL)11 к продакшен приложению, тем самым обезопасив регистрацию. Поскольку мы будем реализовывать SSL на стороне сервера, пример приложения будет также обезопасен для входа пользователя (Глава 8) и также будет устойчив к уязвимости перехвата сессии (Раздел 8.2.1).

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

$ git add .
$ git commit -m "Finish user signup"
$ git checkout master
$ git merge sign-up

Для того чтобы задеплоить приложение, нам для начала нужно добавить строку инициирующую использование SSL в продакшен. Получившийся в результате конфигурационный файл продакшен окружения config/environments/production.rb представлен в Листинге 7.29.

Листинг 7.29. Конфигурирование приложения для использования SSL в продакшен.
config/environments/production.rb
SampleApp::Application.configure do
  .
  .
  .
  # Force all access to the app over SSL, use Strict-Transport-Security,
  # and use secure cookies.
  config.force_ssl = true
  .
  .
  .
end

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

$ git commit -a -m "Add SSL in production"
$ git push heroku

Затем нам нужно запустить миграцию на продакшен базе данных для того чтобы сообщить Heroku о модели данных User:

$ heroku run rake db:migrate

(Вы можете увидеть deprecation предупреждения в этой точке; просто игнорируйте их.)

Наконец, нам необходимо установить SSL на удаленном сервере. Конфигурирование продакшен сайта для использования SSL это довольно неприятная процедура, кроме всего прочего подразумевающая покупку SSL сертификата для вашего домена. К счастью, для приложений запущенных на домене Heroku (таких как наш Пример Приложения), мы можем упасть на хвост SSL сертификату Heroku. Если вы хотите запустить SSL на собственном домене, таком как example.com, вам ничего не остается кроме как немного помучиться, о том как это правильно делать вы можете прочитать на Heroku странице о SSL.

Результатом всей этой работы является рабочая форма регистрации на продакшен сервере (Рис. 7.22):

$ heroku open

Обратите внимание на https:// вместо обычного http:// (Рис. 7.22). Дополнительная ‘s’ это указание на то что SSL работает.

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

$ heroku logs

для того чтобы отловить ошибку с помощью логов Heroku.

signup_in_production_bootstrap
Рис. 7.22: Рабочая страница регистрации в Web. (полный размер)

7.5 Заключение

Возможность регистрировать пользователей это важная веха для нашего приложения. Хотя пример приложения до сих пор не делает ничего полезного, мы заложили необходимый фундамент для последующей разработки. В Главе 8 мы завершим наш механизм аутентификации позволив пользователям входить и выходить из приложения. В Главе 9 позволим пользователям обновлять информацию в их учетных записях и позволим администраторам сайта удалять пользователей тем самым завершив полный набор REST действий ресурса Users из Таблицы 7.1. Наконец, мы добавим методы авторизации к нашим действиям для того чтобы обеспечить безопасность сайта.

7.6 Упражнения

  1. Проверьте что код в Листинге 7.30 позволяет хелперу gravatar_for, определенному в Разделе 7.1.4 принимать опциональный параметр size, позволив код вроде gravatar_for user, size: 40 в представлении.
  2. Напишите тесты для ошибок регистрации реализованных в Листинге 7.23. Стартовые рекомендации представлены в Листинге 7.31.
  3. Написав вначале тест или намеренно ломая, а затем исправляя код приложения, проверьте что тесты в Листинге 7.32 правильно описывают поведение после сохранения пользователя в действии create. Листинг 7.32 использует метов have_selector представленный в упражнениях Главы 5 (Раздел 5.6). В данном случае мы используем have_selector для выбора определенных CSS классов вместе с конкретными HTML тегами.
  4. Как было отмечено ранее, HTML флэша в Листинге 7.27 уродлив. Проверьте, запустив набор тестов, что очищенный код в Листинге 7.33, использующий Rails хелпер content_tag, тоже работает.
Листинг 7.30. Определение необязательного параметра :size для хелпера gravatar_for.
app/helpers/users_helper.rb
module UsersHelper

  # Returns the Gravatar (http://gravatar.com/) for the given user.
  def gravatar_for(user, options = { size: 50 })
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end
Листинг 7.31. Рекомендуемые тесты для сообщений об ошибках.
spec/requests/user_pages_spec.rb
  .
  .
  .
  describe "signup" do

    before { visit signup_path }
    .
    .
    .
    describe "with invalid information" do
      .
      .
      .
      describe "after submission" do
        before { click_button submit }

        it { should have_title('Sign up') }
        it { should have_content('error') }
      end
      .
      .
      .
Листинг 7.32. Тесты описывающие поведение после сохранения в действии create.
spec/requests/user_pages_spec.rb
    .
    .
    .
    describe "with valid information" do
      .
      .
      .
      describe "after saving the user" do
        before { click_button submit }
        let(:user) { User.find_by(email: '[email protected]') }

        it { should have_title(user.name) }
        it { should have_selector('div.alert.alert-success', text: 'Welcome') }
      end
      .
      .
      .
Листинг 7.33. ERb для flash в шаблоне сайта использующий content_tag.
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
      .
      .
      .
      <% flash.each do |key, value| %>
        <%= content_tag(:div, value, class: "alert alert-#{key}") %>
      <% end %>
      .
      .
      .
</html>
  1. Mockingbird не поддерживает кастомные изображения вроде фото на Рис. 7.1; я добавил его вручную с помощью Adobe Fireworks.  
  2. Гиппопотам был взят с http://www.flickr.com/photos/43803060@N00/24308857/
  3. Rails debug информация показана как YAML (a recursive acronym (или рекурсивный акроним)) обозначающий “YAML Ain’t Markup Language”), который является дружественным форматом данных, разработанным так, чтобы быть удобочитаемым и для машин и для людей. 
  4. Вы также можете определять собственные окружения; см. RailsCast on adding an environment
  5. Это означает что маршрутизация работает, но соответствующие страницы необязательно должны работать в этой точке. Например, /users/1/edit правильно направляется к edit действию контроллера Users, но поскольку действие edit пока не существует, обращение к этому URL вернет ошибку. 
  6. В индуизме, аватар это форма проявление божества в человеке или животном. В более широком смысле термин avatar обычно используется для обозначения персонализации пользователя, особенно в виртуальной среде. Но вы уже видели фильм, и почти наверняка знаете об этом. 
  7. Если вашему приложению потребуется обработка кастомных изображений или загрузки других файлов, я рекомендую использовать для этих целей гем Paperclip
  8. Странно, не правда ли? Я тоже этого не понимаю. 
  9. Я выяснил это путем поиска pluralize в Rails API
  10. На самом деле мы будем использовать тесно связанный flash.now, но мы отложим эти тонкости до тех пор пока они нам не понадобятся. 
  11. Технически, SSL это теперь TLS, от Transport Layer Security, но все кого я знаю по-прежнему говорят “SSL”.