Ruby on Rails Tutorial

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

Michael Hartl

Содержание

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

Foreword

Моя компания (CD Baby) была одной из первых громко перешедших на Ruby on Rails, а затем еще громче вернувшейся обратно на PHP (Google расскажет вам об этой драме). Эту книгу, написанную Майклом Хартлом так высоко рекомендовали, что я должен был попробовать её, и Ruby on Rails Tutorial это всё, что я использовал, чтобы вернуться к Rails.

Хотя я уже прошел через много книг по Rails, это одна из немногих, что, наконец, зацепила меня. Было много написано книг типа «Путь Rails» — после которых я чувствовал себя неестественно, но после этой книги я наконец почувствовал себя естественно. Это также единственная книга по Rails, которая соблюдает методику «разработка через тестирование» на всем своем протяжении, этот подход строго рекомендуется специалистами, но он никогда не был так чётко продемонстрирован ранее. Наконец, Git, GitHub и Heroku присутствуют в демо-примерах, автор действительно дает вам почувствовать, что он хотел сделать реальный проект. Учебный код примеров не изолирован.

Линейное повествование — отличный формат. Лично я прошел Rails Tutorial в течении трёх долгих дней, делая все примеры и задачи в конце каждой главы. Делайте всё от начала до конца, не прыгая, и вы получите максимальную пользу.

Наслаждайтесь!

Derek Sivers (sivers.org)
Ранее: основатель, CD Baby
В настоящее время: основатель Thoughts Ltd.

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

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

Я хотел бы поблагодарить огромное количество Рубистов Rubyists учивших и вдохновлявших меня на протяжении многих лет: David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Pratik Naik, Sarah Mei, Sarah Allen, Wolfram Arnold, Alex Chaffee, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, хороших людей из Pivotal Labs, команду Heroku, thoughtbot ребят, и команду GitHub. Наконец, многих, многих читателей - слишком много чтобы перечислять их здесь - внёсших большое количество предложений по улучшению и сообщивших об ошибках во время написания этой книги, и я с благодарностью признаю их помощь в написании ее настолько хорошей, насколько это было возможно.

Об авторе

Майкл Хартл – автор Ruby on Rails Tutorial, лидирующего введения в веб разработку на Ruby on Rails. Его предыдущий опыт включает в себя написание и разработку RailsSpace - чрезвычайно устаревшего учебника по Rails и разработку Insoshi - некогда популярной, а ныне устаревшей платформы для социальных сетей написанной на Ruby on Rails. В 2011, Майкл получил Ruby Hero Award за его вклад в Ruby сообщество. Он закончил Harvard College, имеет степень Кандидата Физических Наук присвоенную в Caltech и является выпускником предпринимательских курсов Y Combinator.

Копирайт и лицензия

Ruby on Rails Tutorial: Learn Web Development with Rails. Copyright © 2012 by Michael Hartl. Весь исходный код в Ruby on Rails Tutorial доступен под MIT License и Beerware License.

Лицензия MIT

Copyright (c) 2012 Michael Hartl

Данная лицензия разрешает лицам, получившим копию данного программного
обеспечения и сопутствующей документации (в дальнейшем именуемыми
«Программное Обеспечение»), безвозмездно использовать Программное
Обеспечение без ограничений, включая неограниченное право на использование,
копирование, изменение, добавление, публикацию, распространение,
сублицензирование и/или продажу копий Программного Обеспечения, также
как и лицам, которым предоставляется данное Программное Обеспечение,
при соблюдении следующих условий:

Указанное выше уведомление об авторском праве и данные условия должны быть
включены во все копии или значимые части данного Программного Обеспечения.

ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО
ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ
ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ
ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ,
ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ
ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
/*
 * ----------------------------------------------------------------------------
 * "ПИВНАЯ ЛИЦЕНЗИЯ" (Ревизия 42):
 * Весь код написан Майклом Хартлом. До тех пор пока вы осознаете это,
 * вы можете делать с ним все что захотите. Если мы когда нибудь
 * встретимся, и если это того стоило, вы можете купить мне
 * пиво в ответ.
 * ----------------------------------------------------------------------------
 */

Глава 6 Моделирование пользователей

Главу 5 мы закончили созданием страницы-заглушки для регистрации пользователей (Раздел 5.4); в течение следующих четырех глав мы выполним обещание, неявное в этой начинающейся странице регистрации. Первый важный шаг это создание модели данных для пользователей нашего сайта, вместе со способом хранить эти данные. В Главе 7 мы дадим пользователям возможность регистрироваться на нашем сайте и создадим страницу профиля пользователя. Как только пример приложения сможет создавать новых пользователей, мы также позволим им входить и выходить (Глава 8) и в Главе 9 (Раздел 9.2.1) мы узнаем как защитить страницы от несанкцонированного доступа. Взятые вместе, материалы с Главы 6 по Главу 9 разрабатывают полную Rails систему входа и аутентификации. Как вы, возможно, знаете, для Rails существует множество готовых решений для аутентификации; Блок 6.1 поясняет почему разворачивание собственной системы является лучшей идеей.

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

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

$ git checkout master
$ git checkout -b modeling-users

(Первая строка здесь только для того чтобы удостовериться, что вы находитесь на master ветке, чтобы тема ветки modeling-users была основана на master ветке. Можно пропустить эту команду, если вы уже находитесь в master ветке.)

6.1 Модель User

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

signup_mockup_bootstrap
Рис. 6.1: Набросок страницы регистрации пользователей. (полный размер)

В Rails дефолтную структуру данных для модели данных называют, что достаточно естественно, модель (М. в MVC из Раздела 1.2.6). Дефолтное решение Rails для проблемы персистентности состоит в том, чтобы использовать базу данных для долгосрочного хранения данных и дефолтную библиотеку Active Record для взаимодействия с базой данных.1 Active Record идет с массой методов для создания, хранения и поиска объектов данных, и все они не требуют использования языка структурированных запросов (SQL)2 применяемого реляционными базами данных. Кроме того, у Rails есть функции, называемые миграциями, которые позволяют писать определения данных на чистом Ruby, без необходимости изучать язык определения данных (DDL). Как результат, Rails почти полностью изолирует вас от деталей хранения данных. В этой книге, благодаря использованию SQLite для разработки и PostgreSQL (через Heroku) для развертывания (Раздел 1.4), мы проработали эту тему еще дальше, до точки, где нам едва ли когда-нибудь придется задумываться о том, как Rails хранит данные, даже для рабочих приложений.

6.1.1 Миграции базы данных

Можно вспомнить из Раздела 4.4.5 , что мы уже встречали, в сделанном нами классе User объекты user с атрибутами name и email. Тот класс служил полезным примером, но он испытывал недостаток в критическом свойстве персистентности: когда мы создали объект User в консоли Rails, он исчез, как только мы вышли. Наша цель в этом Разделе состоит в том, чтобы создать модель для пользователей, которые не будут исчезать так легко.

Как и с классом User в Разделе 4.4.5, мы начнем с моделирования пользователя с двумя атрибутами: name и email, последний мы будем использовать в качестве уникального имени пользователя.3 (Мы добавим атрибут пароля в Разделе 6.3.) В Листинге 4.9 мы сделали это с помощью Ruby-метода attr_accessor:

class User
  attr_accessor :name, :email
  .
  .
  .
end

Напротив, при использовании Rails, для моделирования пользователей мы не должны идентифицировать атрибуты явно. Как было кратко отмечено выше, для хранения данных Rails по умолчанию использует реляционные базы данных, которые состоят из таблиц составленных из строк, данных, где у каждой строки есть столбцы атрибутов данных. Например, для того, чтобы сохранить пользователей с именами и адресами электронной почты, мы составим таблицу users со столбцами name и email (с каждой строкой, соответствующей одному пользователю). Называя столбцы таким образом, мы позволяем Active Record выводить атрибуты объектов User для нас.

Давайте посмотрим как это работает. (Если это обсуждение становится слишком абстрактным на ваш взгляд, будьте терпеливы; консольные примеры, начинающиеся в Разделе 6.1.3 и скриншоты браузера базы данных на Рис. 6.3 и Рис. 6.6 должны многое прояснить.) Вспомните из Листинга 5.28 что мы создавали контроллер Users (наряду с new действием) используя команду

$ rails generate controller Users new --no-test-framework

Есть аналогичная команда для создания модели: generate model. Листинг 6.1 показывает команду для генерации модели User с двумя атрибутами, name и email.

Листинг 6.1. Генерация модели User.
$ rails generate model User name:string email:string
      invoke  active_record
      create    db/migrate/[timestamp]_create_users.rb
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb

(Обратите внимание, что, в отличие от множественного соглашения для имен контроллеров, названия моделей - в ед. числе: контроллер Users, но модель User.) Передавая дополнительные параметры name:string и email:string, мы говорим Rails о двух желаемых атрибутах, наряду с тем, какого типа эти атрибуты должны быть (в данном случае, string). Сравните это с включением имен действий в Листинге 3.4 и Листинге 5.28.

Одним из результатов generate команды в Листинге 6.1 является новый файл, названный migration. Миграции обеспечивают возможность постепенного изменения структуры базы данных, так, чтобы наша модель данных могла адаптироваться к изменяющимся требованиям. В случае модели User, миграция создается автоматически сценарием генерации модели; что создает таблицу users с двумя столбцами, name и email, как это показано в Листинге 6.2. (Мы увидим в Разделе 6.2.5 и еще раз в Разделе 6.3 как создавать миграцию с нуля.)

Листинг 6.2. Миграция для модели User (создающая таблицу users).
db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

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

Сама миграция представляет собой метод change определяющий изменения которые необходимо внести в базу данных. В случае Листинга 6.2, change использует Rails метод называемый create_table для создания таблицы в базе данных для хранения пользователей. Метод create_table принимает блок (Раздел 4.3.2) с одной переменной блока, в данном случае названной t (от “table”). Внутри блока метод create_table использует объект t для создания name и email столбцов в базе данных, оба с типом string.4 Здесь название таблицы во множественном числе (users) даже при том, что название модели в ед. числе (User), что отражает лингвистическое соглашение которому следует Rails: модель представляет единственного (отдельного) пользователя, тогда как таблица базы данных состоит из многих пользователей. Заключительная строка в блоке, t.timestamps, является специальной командой, которая создает два волшебных столбца, называемые created_at и updated_at, которые являются временнЫми отметками, которые автоматически записывают, когда данный пользователь создается и обновляется. (Мы увидим конкретные примеры волшебных столбцов в Разделе 6.1.3.) Полная модель данных, представленная этой миграцией, показана на Рис. 6.2.

user_model_initial
Рис. 6.2: Модель данных "пользователи", произведенная Листингом 6.2.

Мы можем запустить миграцию, известную как “migrating up”, используя rake команду (Блок 2.1) следующим образом:

$ bundle exec rake db:migrate

(Можно вспомнить, что мы запускали эту команду прежде, в Разделе 2.2.) При первом запуске db:migrate она создает файл db/development.sqlite3, который является базой данных SQLite5. Мы можем увидеть структуру базы данных, используя превосходный SQLite Database Browser чтобы открыть файл db/development.sqlite3 (Рис. 6.3); сравните со схемой на Рис. 6.2. Вы могли отметить, что есть один столбец в Рис. 6.3 неучтенный в миграции: столбец id. Как было вкратце отмечено в Разделе 2.2, этот столбец создается автоматически, и используется Rails в качестве уникального идентификатора каждой строки.

sqlite_database_browser
Рис. 6.3: SQLite Database Browser с нашей новой users таблицей. (полный размер)

Большинство миграций являются обратимыми, а это означает что мы можем “migrate down” и переделать ее с помощью единственной Rake задачи, называемой db:rollback:

$ bundle exec rake db:rollback

Под капотом этой Rake задачи происходит выполнение команды drop_table для удаления таблицы users из базы данных. Причина по которой это работает кроется в том, что метод change знает что drop_table это команда обратная create_table и это означает что способ отката миграции легко определим. В случае необратимых миграций, таких как удаление столбца из базы данных, необходимо определять отдельные up и down методы вместо единственного метода change. Почитайте о миграциях в (rus)Rails Guides дабы составить о них более полное представление.

Если вы откатывали базу данных, migrate up снова перед продолжением:

$ bundle exec rake db:migrate

6.1.2 Файл модели

Мы видели, как генерация модели User в Листинге 6.1 сгенерировала файл миграции (Листинг 6.2) и мы видели на Рис. 6.3 результаты выполнения этой миграции: это обновило файл development.sqlite3, создав таблицу users со столбцами id, name, email, created_at и updated_at. Листинг 6.1 также создал саму модель; остальная часть этого раздела посвящена ее изучению.

Мы начнем с рассмотрения кода для модели User, которая живет в файле user.rb в каталоге app/models/ это, мягко выражаясь, очень компактно (Листинг 6.3). (Примечание: строки attr_accessible не будет если вы используете Rails 3.2.2 или более ранние версии. В этом случае, вам необходимо добавить ее в Разделе 6.1.2.2.)

Листинг 6.3. Совершенно новая модель User.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email
end

Вспомните из Раздела 4.4.2 что синтаксис class User < ActiveRecord::Base означает что класс User наследует от ActiveRecord::Base, таким образом у модели User автоматически есть вся функциональность ActiveRecord::Base класса. Конечно, знание этого наследования не приносит пользы, если мы не знаем что содержит ActiveRecord::Base и мы вскоре это исправим. Прежде, чем мы двинемся дальше, тем не менее, необходимо закончить с двумя задачами.

Аннотация модели

Хотя это и не является строго необходимым, вы могли бы счесть удобным аннотировать ваши Rails модели с помощью гема annotate (Листинг 6.4).

Листинг 6.4. Добавление гема annotate в Gemfile.
source 'https://rubygems.org'
.
.
.
group :development, :test do
  gem 'sqlite3', '1.3.5'
  gem 'rspec-rails', '2.11.0'
end


group :development do
  gem 'annotate', '2.5.0'
end

group :test do
  .
  .
  .
end

(Мы поместили гем annotate в блок group :development (аналогичный group :test) потому что аннотации не нужны в production приложениях.) Затем мы устанавливаем его с bundle install:

$ bundle install

Это дает нам команду annotate, которая просто добавляет комментарии, содержащие модель данных к файлу модели:

$ bundle exec annotate
Annotated (1): User

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

Листинг 6.5. Аннотированная модель User.
app/models/user.rb
# == Schema Information
#
# Table name: users
#
#  id         :integer         not null, primary key
#  name       :string(255)
#  email      :string(255)
#  created_at :datetime
#  updated_at :datetime
#

class User < ActiveRecord::Base
  attr_accessible :name, :email
end

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

Доступные атрибуты

Давайте еще раз посмотрим на модель User, уделив в этот раз особое внимание строке attr_accessible (Листинг 6.6). Эта строка говорит Rails какие атрибуты модели доступны, т.e., какие атрибуты могут быть изменены автоматически сторонними пользователями (такими как пользователи, отправляющие запрос через веб-браузер).

Листинг 6.6. Открытие доступа к name и email атрибутам.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email
end

Код в Листинге 6.6 делает совершенно не то что вы подумали. По умолчанию, все атрибуты модели являются доступными. Листинг 6.6 обеспечивает доступность name и email атрибутов — и только name и email атрибуты являются автоматически доступными для внешних пользователей. Мы увидим почему это является важным в Главе 9: использование attr_accessible важно для предотвращения уязвимости массового назначения, к сожалению распространенной и часто серьезной дыре в системе безопасности во многих Rails приложениях.

6.1.3 Создание объектов user

Мы проделали хорошую подготовительную работу, теперь пора воспользоваться ее результатами и узнать об Active Record, играя с нашей недавно созданной моделью User. Как и в Главе 4, наш инструмент - консоль Rails. Так как мы (пока) не хотим производить какие либо изменения в нашей базе данных, мы запустим консоль в sandbox (песочнице):

$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>> 

Как обозначено полезным сообщением “Любые модификации которые вы сделаете откатятся при выходе”, при работе в песочнице, консоль будет “откатывать” (то есть, отменять) любые изменения базы данных, созданные во время сеанса.

В консольной сессии в Разделе 4.4.5 мы создавали нового пользователя с User.new, к которому мы имели доступ только после подгрузки файла example user из Листинга 4.9. С моделями ситуация иная; как вы можете вспомнить из Раздела 4.4.4, Rails консоль автоматически загружает окружение Rails, которое включает модели. Это означает что мы можем создавать новые объекты user без необходимости подгружать что либо:

>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

Мы видим здесь дефолтное представление объекта user, которое выводит на экран те же атрибуты что были показаны в Рис. 6.2 и Листинге 6.5.

Вызванный без параметров, User.new возвращает объект с nil атрибутами. В Разделе 4.4.5 мы спроектировали пример класса User таким образом, чтобы он принимал инициализационный хэш для установки атрибутов объекта; такое решение было обусловлено библиотекой Active Record, которая позволяет инициализировать объекты тем же способом:

>> user = User.new(name: "Michael Hartl", email: "[email protected]")
=> #<User id: nil, name: "Michael Hartl", email: "[email protected]",
created_at: nil, updated_at: nil>

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

Если вы следили за development log, вы, возможно, заметили, что новые строки еще не обнаружились. Это связано с тем, что вызов User.new не касается базы данных; он просто создает новый Ruby объект в памяти. Чтобы сохранить объект user в базе данных, мы вызовем метод save на переменной user:

>> user.save
=> true

Метод saveвозвращает true если сохранение успешно выполнилось и false если сохранение не выполнено. (Сейчас все сохранения должны успешно выполняться; но в Разделе 6.2 мы увидим случаи, когда некоторые из них не сработают.), После сохранения в development log должна появиться строка с командой SQL INSERT INTO "users". Из-за множества методов, предоставляемых Active Record, в этой книге нам не потребуется необработанный SQL и я опущу обсуждение команд SQL с этого времени. Но вы можете многому научиться, наблюдая за логами

Вы, возможно, заметили что у нового объекта user были nil значения для атрибутов id и волшебных столбцов created_at и updated_at. Давайте посмотрим, изменило ли наше save что-нибудь:

>> user
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">

Мы видим что id было присвоено значение 1, в то время как волшебным столбцам были присвоены текущие время и дата.6 В настоящий момент, метки "создан" (created) и "обновлен" (updated) идентичны; мы увидим, что они могут отличаться в Разделе 6.1.5.

Как и с классом User в Разделе 4.4.5, экземпляры модели User предоставляют доступ к своим атрибутам, используя точку:7

>> user.name
=> "Michael Hartl"
>> user.email
=> "[email protected]"
>> user.updated_at
=> Tue, 05 Dec 2011 00:57:46 UTC +00:00

Как мы увидим в Главе 7, часто бывает удобно создать и сохранить модель в два приема, как мы это сделали выше, но Active Record также позволяет вам объединить эти действия в один шаг с User.create:

>> User.create(name: "A Nother", email: "[email protected]")
#<User id: 2, name: "A Nother", email: "[email protected]", created_at:
"2011-12-05 01:05:24", updated_at: "2011-12-05 01:05:24">
>> foo = User.create(name: "Foo", email: "[email protected]")
#<User id: 3, name: "Foo", email: "[email protected]", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">

Обратите внимание: User.create, вместо того чтобы возвратить true или false, возвращает сам объект User который мы можем дополнительно присвоить переменной (такой как foo во второй команде выше).

Команда, обратная create это destroy:

>> foo.destroy
=> #<User id: 3, name: "Foo", email: "[email protected]", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">

Странно, destroy, как и create, возвращает рассматриваемый объект, хотя я не могу вспомнить что когда-либо использовал значение, возвращаемое destroy. Еще более странно то, что destroyенный объект все еще существует в памяти:

>> foo
=> #<User id: 3, name: "Foo", email: "[email protected]", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">

Как мы узнаем, уничтожили ли мы в действительности объект? И как мы можем получить сохраненные и неуничтоженные объекты user из базы данных? Пора узнать, как использовать Active Record, для поиска объектов user.

6.1.4 Поиск объектов user

Active Record предоставляет несколько способов поиска объектов. Давайте используем их, для того, чтобы найти первого пользователя, которого мы создали, и чтобы проверить, что третий пользователь (foo) был уничтожен. Мы начнем с существующего пользователя:

>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">

Здесь мы передали id пользователя в User.find; Active Record вернула пользователя с этим атрибутом id.

Давайте посмотрим, существует ли пользователь с id 3 в базе данных:

>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3

Так как мы уничтожили нашего третьего пользователя в Разделе 6.1.3, Active Record не может найти его в базе данных. Вместо этого find вызывает exception (исключение), которое является способом указать на исключительное событие при выполнении программы, в данном случае, несуществующий Active Record id вызывает исключение ActiveRecord::RecordNotFound.8

В дополнение к универсальному find, Active Record также позволяет нам искать пользователей по определенным атрибутами:

>> User.find_by_email("[email protected]")
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">

Метод find_by_email автоматически создается библиотекой Active Record на основе email атрибута в таблице users. (Как вы догадываетесь, Active Record также создает метод find_by_name.) Поскольку мы будем использовать адреса электронной почты в качестве имен пользователей, этот вид find будет полезен когда мы узнаем, как позволить пользователям регистрироваться на нашем сайте (Глава 7). Если вы беспокоитесь об эффективности find_by_email при большом количестве пользователей, то вы немного забегаете вперед; мы обсудим эту проблему и ее решение в Разделе 6.2.5.

Мы закончим несколькими более общими способами поиска пользователей. Во-первых, first:

>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">

Естественно, first просто возвращает первого пользователя в базе данных. Есть также all:

>> User.all
=> [#<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">,
#<User id: 2, name: "A Nother", email: "[email protected]", created_at:
"2011-12-05 01:05:24", updated_at: "2011-12-05 01:05:24">]

Не сюрприз, что all возвращает массив (Раздел 4.3.1) всех пользователей в базе данных.

6.1.5 Обновление объектов user

После создания объектов мы зачастую хотим их обновить. Есть два основных способа сделать это. Во-первых, мы можем присвоить атрибуты индивидуально, как мы это делали в Разделе 4.4.5:

>> user           # Just a reminder about our user's attributes
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
>> user.email = "[email protected]"
=> "[email protected]"
>> user.save
=> true

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

>> user.email
=> "[email protected]"
>> user.email = "[email protected]"
=> "[email protected]"
>> user.reload.email
=> "[email protected]"

Теперь, когда мы обновили пользователя, волшебные столбцы отличаются, как и обещалось в Разделе 6.1.3:

>> user.created_at
=> "2011-12-05 00:57:46"
>> user.updated_at
=> "2011-12-05 01:37:32"

Второй способ обновить атрибуты заключается в использовании update_attributes:

>> user.update_attributes(name: "The Dude", email: "[email protected]")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "[email protected]"

Update_attributes метод принимает хеш атрибутов и в случае успеха выполняет и обновление, и сохранение за один шаг (возвращающая true чтобы указать что сохранение произошло). Стоит отметить, что, после того как вы определили некоторые атрибуты как доступные, используя attr_accessible (Раздел 6.1.2.2), только эти атрибуты могут быть включены в вызов update_attributes. Если вы когда-либо обнаружите, что ваши модели загадочным образом начинают отказываться обновлять определенные столбцы, удостоверьтесь что эти столбцы включаются в вызов attr_accessible.

6.2 Валидации User

У модели User, которую мы создали в Разделе 6.1 теперь есть рабочие атрибуты name и email, но они абсолютно универсальны: любая строка (включая пустую) в настоящий момент допустима. И все же, имена и адреса электронной почты это нечто более определенное. Например, name не должно быть пробелом, email должен соответствовать определенному формату, характерному для адресов электронной почты. Кроме того, так как мы будем использовать адреса электронной почты в качестве уникальных имен пользователей при регистрации, мы не должны позволять дублироваться адресам электронной почты в базе данных.

Короче говоря, мы не должны позволить name и email быть просто любыми строками; мы должны реализовать определенные ограничения для их значений. Active Record позволяет нам налагать такие ограничения, с помощью validations. В этом разделе мы рассмотрим несколько из наиболее распространенных случаев, применив валидации для наличия, длины, формата и уникальности. В Разделе 6.3.4 мы добавим заключительную общепринятую валидацию, подтверждение. И мы увидим в Разделе 7.3 как валидации дают нам удобные сообщения об ошибках, когда пользователи предоставляют данные, которые нарушают их.

6.2.1 Начальные тесты для пользователей

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

--no-test-framework

при генерации модели User (в отличие, например, от Листинга 5.28), команда в Листинге 6.1 создала начальные спеки для тестирования пользователей, но в данном случае они практически пусты (Листинг 6.7).

Листинг 6.7. Практически пустой дефолтный спек User.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  pending "add some examples to (or delete) #{__FILE__}"
end

Здесь просто используется метод pending для указания на то, что мы должны заполнить спек чем-нибудь полезным. Мы можем увидеть результат его применения запустив спек модели User:

$ bundle exec rspec spec/models/user_spec.rb
*


Finished in 0.01999 seconds
1 example, 0 failures, 1 pending

Pending:
  User add some examples to (or delete)
  /Users/mhartl/rails_projects/sample_app/spec/models/user_spec.rb
  (Not Yet Implemented)

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

Мы последуем совету дефолтного спека и заполним его небольшим количеством RSpec примеров, как это показано в Листинге 6.8.

Листинг 6.8. Тестирование :name и :email атрибутов.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before { @user = User.new(name: "Example User", email: "[email protected]") }

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
end

Блок before, который мы видели в Листинге 5.27), запускает код внутри блока перед каждым тестом, в данном случае, создавая новую переменную экземпляра @user с помощью User.new и валидного инициализационного хэша. Затем

subject { @user }

делает @user дефолтным cубъектом тестирования, как мы это видели прежде в контексте переменной page в Разделе 5.3.4.

Два теста в Листинге 6.8 тестируют на наличие name и email атрибутов:

it { should respond_to(:name) }
it { should respond_to(:email) }

Эти тесты неявно используют Ruby метод respond_to?, который принимает символ и возвращает true в случае если объект отвечает на данный метод или атрибут и возвращает false в противном случае:

$ rails console --sandbox
>> user = User.new
>> user.respond_to?(:name)
=> true
>> user.respond_to?(:foobar)
=> false

(Вспомните из Раздела 4.2.3 что Ruby использует знак вопроса для обозначения таких true/false булевых методов.) Сами тесты опираются на булевую конвенцию используемую RSpec: код

@user.respond_to?(:name)

может быть протестирован с помощью такого RSpec кода

@user.should respond_to(:name)

Благодаря subject { @user }, мы можем опустить @user в тесте, что приведет к

it { should respond_to(:name) }

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

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

$ bundle exec rspec spec/

Несмотря на то что мы создали базу данных для разработки командой rake db:migrate в Разделе 6.1.1, тесты провальны, поскольку тестовая база данных пока не знает о модели данных (мало того, она пока вообще не существует). Мы можем создать тестовую базу данных с правильной структурой и тем самым получить прохождение тестов с помощью Rake задачи db:test:prepare:

$ bundle exec rake db:test:prepare

Это просто обеспечивает соответствие между моделью данных базы данных для разработки в db/development.sqlite3 и моделью данных тестовой базы данных в db/test.sqlite3. Незапуск этой Rake задачи после миграции является частым источником недоразумений. К тому же, иногда тестовая база данных выходит из строя и требуется ее рестартовать. Если ваш набор тестов загадочным образом рухнул, попробуйте запустить rake db:test:prepare - возможно это решит проблему.

6.2.2 Валидация наличия

Возможно самой элементарной валидацией является валидация наличия, которая просто проверяет что данный атрибут имеется в наличии. Например, в даннам разделе мы убеждаемся что оба name и email поля заполнены прежде чем пользователь будет сохранен в базе данных. В Разделе 7.3.2 мы увидим как распространить это требование на форму регистрации новых пользователей.

Мы начнем с теста на наличие атрибута name. Хотя первым шагом в TDD является написание провального теста (Раздел 3.2.1), в данном случае мы пока недостаточно знаем о валидации для того чтобы написать годный тест, так что мы вначале напишем валидацию и немного поиграем с ней в консоли, - чтобы побольше узнать о ней. Затем мы закомментируем валидацию, напишем провальный тест и проверим что раскомментирование валидации приводит к прохождению теста. Эта процедура может показаться излишне педантичной для такого простого теста, но я видел много “простых” тестов, которые на самом деле тестировали неправильные вещи; дотошность в TDD это просто единственный способ быть уверенными в том что мы тестируем правильные вещи. (Такая техника закомментирования также полезна при спасении приложения, код которого уже написан, но—quelle horreur! — (# какой ужас - фр.) не имеет тестов.)

Способ валидации наличия атрибута имени заключается в применении метода validates с аргументом presence: true, как это показано в Листинге 6.9. Аргумент presence: true это одноэлементный хэш опций; вспомните из Раздела 4.3.4 что фигурные скобки являются необязательными при передаче хеша в качестве последнего аргумента в методе. (Как отмечено в Разделе 5.1.1, использование хэшэй опций это очень распространенный прием в Rails.)

Листинг 6.9. Валидация наличия name атрибута.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email

  validates :name, presence: true
end

Листинг 6.9 возможно выглядит как магия, но validates это просто метод, как впрочем и attr_accessible. Эквивалентная Листингу 6.9 формулировка с применением скобок выглядит следующим образом:

class User < ActiveRecord::Base
  attr_accessible(:name, :email)

  validates(:name, presence: true)
end

Давайте заскочим в консоль чтобы увидеть эффект добавления валидации к нашей модели User:9

$ rails console --sandbox
>> user = User.new(name: "", email: "[email protected]")
>> user.save
=> false
>> user.valid?
=> false

Здесь user.save возвращает false, указывая на провальное сохранение. В заключительной команде мы используем valid? метод, который возвращает false когда объект приводит к сбою одной или более валидаций, и true когда все валидации проходят. В данном случае у нас есть только одна валидация, таким образом, мы знаем, какая именно провалилась, но все же не лишним будет в этом убедиться с помощью объекта errors, генерируемого при отказе:

>> user.errors.full_messages
=> ["Name can't be blank"]

(Сообщение об ошибке - подсказка, говорящая о том что Rails проверяет наличие атрибута, используя blank? метод, который мы видели в конце Раздела 4.4.3.)

Теперь о провальном тесте. Чтобы гарантировать что наш начальный тест перестанет работать, давайте закомментируем валидацию (Листинг 6.10).

Листинг 6.10. Закомментирование валидации для обеспечения провальности теста.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email

  # validates :name, presence: true
end

Начальный тест валидации представлен в Листинге 6.11.

Листинг 6.11. Провальный тест валидации атрибута name.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]")
  end

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }

  it { should be_valid }

  describe "when name is not present" do
    before { @user.name = " " }
    it { should_not be_valid }
  end
end

Первый новый тест это просто проверка на то что объект @user изначально валиден:

it { should be_valid }

Это еще один пример булевой конвенции RSpec которую мы видели ранее в Разделе 6.2.1: в каждом случае, когда объект отвечает на булевый метод foo?, существует соответствующий тестовый метод с именем be_foo. В данном случае мы можем протестировать результат вызова

@user.valid?

с помощью

@user.should be_valid

Как и прежде, subject { @user } позволяет нам опустить @user, что приводит к

it { should be_valid }

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

describe "when name is not present" do
  before { @user.name = " " }
  it { should_not be_valid }
end

Здесь используется блок before для назначения невалидного значения атрибуту name, а затем происходит проверка того что получившийся объект user невалиден.

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

$ bundle exec rspec spec/models/user_spec.rb
...F
4 examples, 1 failure

Теперь раскомментируем валидацию (т.е., вернемся от Листинга 6.10 обратно к Листингу 6.9) для того чтобы получить прохождение теста:

$ bundle exec rspec spec/models/user_spec.rb
....
4 examples, 0 failures

Конечно, мы также хотим валидировать наличие адресов электронной почты. Тест (Листинг 6.12) походит на аналогичный тест для атрибута name.

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

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  describe "when email is not present" do
    before { @user.email = " " }
    it { should_not be_valid }
  end
end

Реализация практически та же, что и в Листинге 6.13.

Листинг 6.13. Валидация наличия атрибутов name и email.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email

  validates :name,  presence: true
  validates :email, presence: true
end

Теперь все тесты должны проходить и валидации “наличия” готовы.

6.2.3 Валидация длины

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

Мы начнем с теста. В выборе максимальной длины нет ничего хитрого; мы просто примем 50 как разумную верхнюю границу, что означает что имена длиной в 51 символ будут слишком длинными (Листинг 6.14).

Листинг 6.14. Тест для валидации длины name.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  describe "when name is too long" do
    before { @user.name = "a" * 51 }
    it { should_not be_valid }
  end
end

Для удобства мы использовали “мультипликацию строки” в Листинге 6.14 для создания строки длиной в 51 символ. Мы можем увидеть как это работает, используя консоль:

>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51

Тест в Листинге 6.14 должен провалиться. Чтобы заставить его пройти, мы должны знать об аргументе валидации, ограничивающим длину, :length, наряду с :maximum параметром реализуют верхнюю границу (Listing 6.15).

Листинг 6.15. Добавление валидации длины для name атрибута.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email

  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true
end

С нашим комплектом тестов, вновь проходящим, мы можем идти дальше, к более интересной валидации: валидации формата электронной почты.

6.2.4 Валидация формата

Наши валидации для атрибута name реализуют только минимальные ограничения: любое непустое имя длиной до 51 символов пройдет; но, конечно, атрибут email должен соответствовать более строгим требованиям. До сих пор мы отклоняли только пустой адрес электронной почты; в этом разделе мы потребуем, чтобы адреса электронной почты соответствовали знакомому образцу [email protected].

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

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[[email protected] [email protected] [email protected]]
=> ["[email protected]", "[email protected]", "[email protected]"]
>> addresses.each do |address|
?>   puts address
>> end
[email protected]
[email protected]
[email protected]

Здесь мы выполнили итерации по элементам массива addresses используя each метод (Раздел 4.3.2). Вооружившись этой техникой мы готовы написать несколько базовых тестов для валидации формата электронной почты (Листинг 6.16).

Листинг 6.16. Тесты для валидации формата адреса электронной почты.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  describe "when email format is invalid" do
    it "should be invalid" do
      addresses = %w[user@foo,com user_at_foo.org example.user@foo.
                     foo@bar_baz.com foo@bar+baz.com]
      addresses.each do |invalid_address|
        @user.email = invalid_address
        @user.should_not be_valid
      end
    end
  end

  describe "when email format is valid" do
    it "should be valid" do
      addresses = %w[[email protected] [email protected] [email protected] [email protected]]
      addresses.each do |valid_address|
        @user.email = valid_address
        @user.should be_valid
      end
    end
  end
end

Как было отмечено выше, они не являются исчерпывающими, но мы проверили обычные допустимые формы электронной почты [email protected], [email protected] (верхний регистр, подчеркивание и соединенные домены) и [email protected] (стандартное корпоративное имя пользователя first.last, с двухбуквенным доменом верхнего уровня jp), наряду с несколькими недопустимыми формами.

Код приложения для валидации формата электронной почты использует регулярное выражение (или regex) для определения формата, наряду с :format аргументом для validates метода (Листинг 6.17).

Листинг 6.17. Валидация формата адреса электронной почты с регулярным выражением.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email

  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end

Здесь регулярное выражение VALID_EMAIL_REGEX это константа, которая обозначается в Ruby именем начинающимся с большой буквы. Код

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }

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

Так, откуда появился образец? Регулярные выражения состоят из краткого (некоторые сказали бы нечитаемого) языка для сравнения текстовых шаблонов; изучение построения регулярных выражений это искусство и для начала я разбил VALID_EMAIL_REGEX на небольшие куски (Таблица 6.1).10 Я считаю что замечательный онлайн редактор регулярных выражений Rubular (Рис. 6.4) просто незаменим для изучения регулярных выражений.11 Cайт Rubular имеет красивый интерактивный интерфейс для создания регулярных выражений, а также удобную Regex справку. Я призываю вас изучать Таблицу 6.1 с открытым в браузере Rubular-ом. Никакое чтение о регулярных выражениях не может заменить пару часов игры с Rubular. (Примечание: если вы хотите использовать регулярное выражение из Листинга 6.17 в Rubular, вам следует пропустить символы \A и \z.)

ВыражениеЗначение
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/iполное регулярное выражение
/начало регулярного выражения
\Aначало соответствия строки
[\w+\-.]+по крайней мере один символ слова, плюс, дефис или точка
@буквально “знак собаки”
[a-z\d\-.]+по крайней мере одна буква, цифра, дефис или точка
\.буквальная точка
[a-z]+по крайней мере одна буква
\zконец соответствия строки
/конец регулярного выражения
iнечувствительность к регистру
Таблица 6.1: Элементы регулярного выражения для регулярного выражения для email из Листинга 6.17.

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

rubular
Рис. 6.4: Удивительный редактор регулярных выражений Rubular(полный размер)

Теперь тесты должны пройти. (Фактически, тесты для валидных адресов электронной почты должны были проходить все время; так как регулярные выражения, как известно, подвержены ошибкам, действительные испытания электронной почты в основном заключаются в санитарной проверке на VALID_EMAIL_REGEX.) Это означает, что осталось только одно ограничение: обеспечение уникальности адресов электронной почты.

6.2.5 Валидация уникальности

Для обеспечения уникальность адресов электронной почты (так, чтобы мы могли использовать их в качестве имен пользователей) мы будем использовать :unique опцию для validates метода. Но предупреждаю: есть важное предостережение, так что не просто просмотрите раздел, а прочитайте его внимательно.

Мы начнем, как обычно, с наших тестов. В наших предыдущих тестах модели мы, главным образом, использовали User.new, который только создает объект Ruby в памяти, но для тестов уникальности мы фактически должны поместить запись в базу данных.13 (Первый) тест дублирования электронной почты представлен в Листинге 6.18.

Листинг 6.18. Тест на отклонение повторяющихся адресов электронной почты.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.save
    end

    it { should_not be_valid }
  end
end

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

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

Листинг 6.19. Валидация уникальности адресов электронной почты.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end

И все же мы не закончили. Адреса электронной почты не чувствительны к регистру — [email protected] равен [email protected] или [email protected] — и наша валидация должна учитывать и этот случай. Мы тестируем на это с помощью кода из Листинга 6.20.

Листинг 6.20. Нечувствительный к регистру тест на отклонение дублирующихся адресов электронной почты.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]")
  end
  .
  .
  .
  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.email = @user.email.upcase
      user_with_same_email.save
    end

    it { should_not be_valid }
  end
end

Здесь мы используем upcase метод на строках (описан кратко в Разделе 4.3.2). Этот тест делает то же самое что и первый тест на дублирование адресов электронной почты, но с прописным адресом электронной почты. Если этот тест кажется вам немного абстрактным, запустите консоль:

$ rails console --sandbox
>> user = User.create(name: "Example User", email: "[email protected]")
>> user.email.upcase
=> "[email protected]"
>> user_with_same_email = user.dup
>> user_with_same_email.email = user.email.upcase
>> user_with_same_email.valid?
=> true

Конечно, сейчас user_with_same_email.valid? является true, так как это провальный тест, но мы хотим, чтобы оно было false. К счастью, :uniqueness принимает опцию, :case_sensitive, как раз для этой цели (Листинг 6.21).

Листинг 6.21. Валидация уникальности адресов электронной почты, игнорирующая регистр.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

Обратите внимание: мы просто заменили true на case_sensitive: false; Rails в этом случае делает вывод, что :uniqueness должно быть true. В этой точке наше приложение обеспечивает уникальность адресов электронной почты и наш набор тестов должен пройти.

Предостережение уникальности

Есть одна небольшая проблема, предостережение, на которое я ссылался выше:

Использование validates :uniqueness не гарантирует уникальности.

D’oh! Но что может может пойти не так? А вот что:

  1. Алиса регистрируется на сайте, с email адресом [email protected].
  2. Алиса случайно кликает, “Submit” дважды, отправляя два запроса в быстрой последовательности.
  3. Затем происходит следующее: первый запрос создает пользователя в памяти, который проходит проверку, второй запрос делает то же самое, первый запрос пользователя сохраняется, второй запрос пользователя сохраняется.
  4. Результат: две пользовательские записи с одинаковыми адресами электронной почты, несмотря на валидацию уникальности.

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

Индекс адреса электронной почты представляет собой обновление требований к нашей модели данных что (как обсуждалось в Разделе 6.1.1) делается в Rails посредством миграций. Мы видели в Разделе 6.1.1 что генерация модели User автоматически создает новую миграцию (Листинг 6.2); в данном случае мы добавляем структуру к существующей модели, таким образом, мы должны создать миграцию непосредственно, используя migration генератор:

$ rails generate migration add_index_to_users_email

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

Листинг 6.22. Миграция для реализации уникальности адреса электронной почты.
db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end

Здесь используется Rails метод add_index для добавления индекса на столбце email таблицы users. Индекс сам по себе не обеспечивает уникальность, но это делает опция unique: true.

Заключительный шаг должен мигрировать базу данных:

$ bundle exec rake db:migrate

(Если это не сработало, попробуйте закрыть все консольные сессии в песочнице, которая может блокировать базу данных, тем самым препятствуя миграции.) Если вам интересно посмотреть на практический результат выполнения этой команды, посмотрите файл db/schema.rb, который теперь должен содержать строку подобную этой:

add_index "users", ["email"], :name => "index_users_on_email", :unique => true

К сожалению, есть еще одно изменение которое мы должны сделать для того чтобы быть уверенными в уникальности email адресов - все email адреса должны быть в нижнем регистре, прежде чем они будут сохранены в базе данных. Причина заключается в том что не все адаптеры баз данных используют регистрозависимые индексы.15 Мы можем достигнуть этого с помощью функции обратного вызова, которая является методом, который вызывается большинстве в конкретный момент жизни объекта Active Record (см. Rails API). В данном случае мы будем использовать функцию обратного вызова before_save для того чтобы принудить Rails переводить в нижний регистр email атрибут перед сохранением пользователя в базу данных, как это показано в Листинге 6.23.

Listing 6.23. Ensuring email uniqueness by downcasing the email attribute.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email

  before_save { |user| user.email = email.downcase }
  .
  .
  .
end

Код в Листинге 6.23 передает блок в коллбэк before_save и назначает email адрес пользователя равным его текущему значению в нижнем регистре с помощью метода строки downcase. Этот код довольно продвинутый и в этой точке я советую вам просто поверить в то что он работает; если вы все же сомневаетесь, закомментируйте валидацию уникальности из Листинга 6.19 и попробуйте создать пользователей с идентичными email адресами для того чтобы посмотреть на результирующую ошибку. (Мы вновь увидим эту технику в Разделе 8.2.1.)

Теперь вышеописанный сценарий с Алисой будет хорошо работать: база данных сохранит запись пользователя, основанную на первом запросе, и отвергнет второе сохранение за нарушение уникальности. (Ошибка появится в логе Rails, но в этом нет ничего плохого. Можно даже отловить ActiveRecord::StatementInvalid исключение — см. Insoshi для примера — но в этом учебном руководстве мы не будем заморачиваться этим шагом.) Добавление этого индекса на атрибут адреса электронной почты преследует вторую цель, кратко рассмотренную в Разделе 6.1.4: он решает проблему эффективности поиска пользователя с помощью find_by_email (Блок 6.2).

6.3 Добавление безопасного пароля

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

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

БОльшая часть механики безопасного пароля будет реализовано с помощьо одного Rails-метода называемого has_secure_password (впервые был представлен в Rails 3.1). Поскольку очень многое в дальнейшем зависит от этого единственного метода, трудно разрабатывать безопасные пароли постепенно. В результате чего, начиная с Раздела 6.3.2 мы напишем большое количество тестов прежде чем какой либо из них пройдет. Если вы начнете вязнуть, я рекомендую набраться терпения и продержаться до конца, в Разделе 6.3.4 нас ждет отличная развязка. (Поскольку скринкасты позволяют демонстрировать более постепенный подход к разработке, заинтересованным читателям следует посмотреть Ruby on Rails Tutorial screencasts для более полного понимания этого материала.)

6.3.1 Зашифрованный пароль

Мы начнем с необходимого изменения модели данных для пользователей, что подразумевает добавление password_digest столбца в таблицу users (Рис. 6.5). Название digest пришло из терминологии криптографических хэш функций, а само имя password_digest необходимо для работы реализации в Разделе 6.3.4. Как следует зашифровав пароль, мы обеспечим невозможность получения доступа к сайту атакером, даже если он умудрится получить копию базы данных.

user_model_password_digest
Рис. 6.5: Модель User с добавленным атрибутом password_digest.

Мы будем использовать самую новомодную хэш функцию, называемую bcrypt для необратимого шифрования пароля в виде хэша пароля. Для того чтобы использовать bcrypt в примере приложения нам необходимо добавить гем bcrypt-ruby в наш Gemfile (Листинг 6.24).

Листинг 6.24. Добавление bcrypt-ruby в Gemfile.
source 'https://rubygems.org'

gem 'rails', '3.2.13'
gem 'bootstrap-sass', '2.1'
gem 'bcrypt-ruby', '3.0.1'
.
.
.

Затем запускаем bundle install:

$ bundle install

На некоторых системах вы можете получить предупреждение

make: /usr/bin/gcc-4.2: No such file or directory

Для того чтобы исправить это, переустановите RVM используя флаг clang:

$ rvm reinstall 1.9.3 --with-gcc=clang

Поскольку мы хотим чтобы пользователи имели столбец password digest, объект user должен отвечать на password_digest, что приводит нас к тесту показанному в Листинге 6.25.

Листинг 6.25. Проверка того, что объект User имеет столбец password_digest.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]")
  end

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
  it { should respond_to(:password_digest) }
  .
  .
  .
end

Для того чтобы получить прохождение этого теста мы вначале генерируем соответствующую миграцию для столбца password_digest:

$ rails generate migration add_password_digest_to_users password_digest:string

Здесь первый аргумент это название миграции и мы также предоставили второй аргумент с названием и типом атрибута который мы хотим создать. (Сравните это с начальной генерацией таблицы users в Листинге 6.1.) Мы можем выбрать любое название для миграции, но было бы удобно, если бы ее название заканчивалось на _to_users, поскольку в этом случае Rails автоматически построит миграцию для добавления столбцов к таблице users. Кроме того, включив второй аргумент, мы дали Rails достаточно информации для построения для нас всей миграции, как это видно в Листинге 6.26.

Листинг 6.26. Миграция для добавления столбца password_digest к таблице users.
db/migrate/[ts]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration
  def change
    add_column :users, :password_digest, :string
  end
end

Этот код использует метод add_column для добавления столбца password_digest к таблице users.

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

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare
$ bundle exec rspec spec/

6.3.2 Пароль и подтверждение

Как видно на наброске Рис. 6.1, мы ожидаем что пользователи должны будут подтверждать их пароли, что является общепринятой в сети практикой минимизирующей риск опечаток при введении пароля. Мы можем реализовать это на уровне контроллера, но принято делать это в модели и использовать Active Record для наложения этого ограничения. Метод заключается в добавлении password и password_confirmation атрибутов к модели User и последующем требовании совпадения этих двух атрибутов перед сохранением записи в базе данных. В отличие от всех остальных атрибутов, что мы видели до этого, атрибуты пароля будут виртуальными — они будут лишь временно существовать в памяти и не будут постоянно храниться в базе данных. Как мы увидим в Разделе 6.3.4, has_secure_password реализует эти виртуальные атрибуты автоматически .

Мы начнем с respond_to тестов для пароля и его подтверждения, как это показано в Листинге 6.27.

Листинг 6.27. Тестирование атрибутов password и password_confirmation.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
  it { should respond_to(:password_digest) }
  it { should respond_to(:password) }
  it { should respond_to(:password_confirmation) }

  it { should be_valid }
  .
  .
  .
end

обратите внимание - мы добавили :password и :password_confirmation в хэш инициализации для User.new:

before do
  @user = User.new(name: "Example User", email: "[email protected]",
                   password: "foobar", password_confirmation: "foobar")
end

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

describe "when password is not present" do
  before { @user.password = @user.password_confirmation = " " }
  it { should_not be_valid }
end

Поскольку мы будем вскоре тестировать несовпадение пароля, здесь мы будем тестировать существование валидации наличия установив пароль и его подтверждение равными чистой строке. Здесь используется возможность Ruby делать более чем одно назначение в строке. Например, в консоли мы можем назначить a и b равными 3 следующим образом:

>> a = b = 3
>> a
=> 3
>> b
=> 3

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

@user.password = @user.password_confirmation = " "

Мы также хотим убедиться что пароль и его подтверждение совпадают. Случай, когда они совпадают покрыт с помощью it { should be_valid }, так что нам осталось протестировать только случай несовпадения:

describe "when password doesn't match confirmation" do
  before { @user.password_confirmation = "mismatch" }
  it { should_not be_valid }
end

В принципе, мы закончили, но есть один случай который мы не обработали. Что если подтверждение пароля пустое? Если оно пустое или состоит из пробелов, но при этом пароль валиден, значит они не совпадают и валидация подтверждения отловит этот случай. Если и пароль и его подтверждение пусты или состоят из пробелов, валидация наличия пароля отловит и этот случай. К сожалению, есть еще один возможный вариант, вариант при котором подтверждение пароля является nil. Это невозможно реализовать через веб-интерфейс, но вполне возможно сделать в консоли:

$ rails console
>> User.create(name: "Michael Hartl", email: "[email protected]",
?>             password: "foobar", password_confirmation: nil)

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

describe "when password confirmation is nil" do
  before { @user.password_confirmation = nil }
  it { should_not be_valid }
end

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

Собрав все вместе, мы получаем (провальные) тесты в Листинге 6.28. Как было отмечено во введении к этому разделу - трудно разрабатывать безопасные пароли постепенно из-за большого количества фич запрятанных в has_secure_password, так что в этой точке все вновь добавленные тесты являются провальными. Мы получим их прохождение в Разделе 6.3.4.

Листинг 6.28. Тест для пароля и его подтверждения.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
  it { should respond_to(:password_digest) }
  it { should respond_to(:password) }
  it { should respond_to(:password_confirmation) }

  it { should be_valid }
  .
  .
  .
  describe "when password is not present" do
    before { @user.password = @user.password_confirmation = " " }
    it { should_not be_valid }
  end

  describe "when password doesn't match confirmation" do
    before { @user.password_confirmation = "mismatch" }
    it { should_not be_valid }
  end

  describe "when password confirmation is nil" do
    before { @user.password_confirmation = nil }
    it { should_not be_valid }
  end
end

6.3.3 Аутентификация пользователя

Последняя часть механики наших паролей это метод для получения пользователей по их email и паролям. Эта задача естественным образом разбивается на две части: первая из них это поиск пользователя по адресу электронной почты; вторая это аутентификация пользователя с данным паролем.

Первый шаг прост; как мы видели в Разделе 6.1.4, мы можем найти пользователя с данным адресом электронной почты с помощью метода find_by_email:

user = User.find_by_email(email)

Второй шаг заключается в применении метода authenticate для проверки того что у пользователя есть данный пароль. В Главе 8, мы будем получать текущего (вошедшего) пользователя используя код вроде этого:

current_user = user.authenticate(password)

Если данный пароль совпадает с паролем пользователя, он должен вернуть пользователя; в противном случае он должен вернуть false.

Как обычно, мы можем выразить требования для authenticate используя RSpec. Получившиеся в результате тесты являются немного более продвинутыми чем те что мы видели до этого, так что давайте разобьем их на части; если вы новичок большинстве в RSpec, вам возможно понадобится прочитать этот раздел несколько раз. Мы начнем с того, что объект User должен отвечать на authenticate:

it { should respond_to(:authenticate) }

Затем мы покрываем два случая - совпадение и несовпадения пароля:

describe "return value of authenticate method" do
  before { @user.save }
  let(:found_user) { User.find_by_email(@user.email) }

  describe "with valid password" do
    it { should == found_user.authenticate(@user.password) }
  end

  describe "with invalid password" do
    let(:user_for_invalid_password) { found_user.authenticate("invalid") }

    it { should_not == user_for_invalid_password }
    specify { user_for_invalid_password.should be_false }
  end
end

Блок before сохраняет пользователя в базе данных, так что он может быть получен с помощью find_by_email, чего мы достигаем используя let method:

let(:found_user) { User.find_by_email(@user.email) }

Мы уже использовали let в нескольких упражнениях, это первый случай когда мы его видим в основном тексте учебника. Блок 6.3 рассказывает о let более подробно.

Два блока describe покрывают случаи когда @user и found_user должны быть одинаковыми (совпадение пароля) и разными (несовпадение пароля); они используют “двойное равенства” == для проверки эквивалентности объектов (Раздел 4.3.1). Обратите внимание что тесты в

describe "with invalid password" do
  let(:user_for_invalid_password) { found_user.authenticate("invalid") }

  it { should_not == user_for_invalid_password }
  specify { user_for_invalid_password.should be_false }
end

используют let второй раз, а также используют метод specify. Это просто синоним для it, который может быть использован когда it звучит ненатурально. В данном случае, хорошо звучит “it [т.е., пользователь] should not equal wrong user”, но “user: user with invalid password should be false” звучало бы странно; “specify: user with invalid password should be false” звучит лучше.

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

describe "with a password that's too short" do
  before { @user.password = @user.password_confirmation = "a" * 5 }
  it { should be_invalid }
end

Собрав вместе все тесты мы получаем Листинг 6.29.

Листинг 6.29. Тесты для метода authenticate.
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "[email protected]",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }
  .
  .
  .
  it { should respond_to(:authenticate) }
  .
  .
  .
  describe "with a password that's too short" do
    before { @user.password = @user.password_confirmation = "a" * 5 }
    it { should be_invalid }
  end

  describe "return value of authenticate method" do
    before { @user.save }
    let(:found_user) { User.find_by_email(@user.email) }

    describe "with valid password" do
      it { should == found_user.authenticate(@user.password) }
    end

    describe "with invalid password" do
      let(:user_for_invalid_password) { found_user.authenticate("invalid") }

      it { should_not == user_for_invalid_password }
      specify { user_for_invalid_password.should be_false }
    end
  end
end

Как было отмечено в Блоке 6.3, let мемоизирует свое значение, так что первый вложенный describe блок в Листинге 6.29 вызывает let для получения пользователя из базы данных с помощью find_by_email, но второй describe блок уже не обращается к базе данных.

6.3.4 У пользователя есть безопасный пароль

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

Во-первых, нам необходимо сделать столбцы password и password_confirmation доступными (Раздел 6.1.2.2) чтобы мы могли создать экземпляр нового пользователя с помощью инициализационного хэша:

@user = User.new(name: "Example User", email: "[email protected]",
                 password: "foobar", password_confirmation: "foobar")

Следуя модели в Листинге 6.6, мы сделаем это добавив соответствующие символы к списку доступных атрибутов:

attr_accessible :name, :email, :password, :password_confirmation

Во-вторых, нам необходима валидация длины для пароля, которая использует ключ :minimum по аналогии с ключом :maximum из Листинга 6.15:

validates :password, presence: true, length: { minimum: 6 }

Затем нам нужно добавить к атрибутам password и password_confirmation требование наличия пароля, требование их совпадения и добавить authenticate метод для сравнения зашифрованного пароля с password_digest для аутентификации пользователей. Это единственный непростой шаг и в последней версии Rails все эти фичи бесплатно поставляются в одном методе - has_secure_password:

has_secure_password

Пока столбец password_digest присутствует в базе данных, добавление одного лишь этого метода к нашей модели дает нам безопасный способ для создания и аутентификации новых пользователей.

(Если вы хотите увидеть как реализован has_secure_password, я советую взглянуть на хорошо документированый и вполне читабельный исходный код secure_password.rb. Этот код включает строку

validates_confirmation_of :password

которая (как описано в Rails API) автомагически создает атрибут password_confirmation. Он также включает валидацию для password_digest атрибута. В Главе 7, мы увидим что это довольно неоднозначное решение.)

Наконец, нам необходима валидация наличия для подтверждения пароля:

validates :password_confirmation, presence: true

Собрав эти три элемента вместе мы приходим к модели User показанной в Листинге 6.30, которая завершает реализацию безопасных паролей.

Листинг 6.30. Законченная реализация безопасных паролей.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password

  before_save { |user| user.email = email.downcase }

  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence:   true,
                    format:     { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :password, presence: true, length: { minimum: 6 }
  validates :password_confirmation, presence: true
end

Теперь необходимо убедиться что набор тестов проходит:

$ bundle exec rspec spec/

6.3.5 Создание пользователя

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

Поскольку мы пока не можем зарегистрироваться через веб-интерфейс — это является целью Главы 7 — мы будем использовать Rails консоль для создания нового пользователя вручную. В отличие от Раздела 6.1.3, в этом разделе мы не должны работать в песочнице, поскольку в этот раз нашей целью является сохранение записи в базе данных:

$ rails console
>> User.create(name: "Michael Hartl", email: "[email protected]",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-07 03:38:14", updated_at: "2011-12-07 03:38:14",
password_digest: "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJgjlw0G4f21y..."> 

Для того чтобы проверить что это работает, давайте взглянем на строку в девелопмент базде данных (db/development.sqlite3) с помощью SQLite Database Browser (Рис. 6.6). Обратите внимание, что столбцы соответствуют атрибутам модели данных, определенной на Рис. 6.5.

sqlite_user_row_with_password
Рис. 6.6: Строка пользователя в базе данных SQLite db/development.sqlite3(полный размер)

Вернувшись в консоль, мы пожем увидеть эффект has_secure_password из Листинга 6.30 посмотрев на атрибут password_digest:

>> user = User.find_by_email("[email protected]")
>> user.password_digest
=> "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJgjlw0G4f21yZIMSH/xoy"

Это зашифрованная версия пароля ("foobar") которая была использована для инициализации объекта user. Мы также можем проверить что команда authenticate работает, использовав вначале невалидный, а затем валидный пароли:

>> user.authenticate("invalid")
=> false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-07 03:38:14", updated_at: "2011-12-07 03:38:14",
password_digest: "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJgjlw0G4f21y..."> 

Как и требовалось, authenticate возвращает false если пароль невалиден и самого пользователя в противном случае.

6.4 Заключение

Начав с нуля, в этой главе мы создали рабочую модель User с name, email и различными "парольными" атрибутами, вместе с валидациями обеспечивающими несколько важных ограничений на их значения. Кроме того, мы можем безопасно аутентифицировать пользователей с помощью пароля. В предыдущих версиях Rails такой подвиг потребовал бы в два раза большего количества кода, но благодаря компактному методу validates и has_secure_password, мы смогли построить рабочую модель User всего лишь в десять строк исходного кода.

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

Если вы используете Git, было бы неплохо закоммитить внесенные изменения, если вы этого еще не сделали:

$ git add .
$ git commit -m "Make a basic User model (including secure passwords)"

Затем опять объединить их с мастер веткой:

$ git checkout master
$ git merge modeling-users

6.5 Упражнения

  1. Добавьте тесты для кода отвечающего за перевод email в нижний регистр из Листинга 6.23, как это показано в Листинге 6.31. Закомменирова строку before_save убедитесь что тесты из Листинга 6.31 тестируют правильные вещи.
  2. Запустив набор тестов убедитесь что коллбэк before_save может быть написан способом показанным в Листинге 6.32.
  3. Прочитайте Rails API введение для ActiveRecord::Base для того чтобы получить представление о ее возможностях.
  4. Изучите Rails API введение для метода validates для того чтобы больше узнать о его возможностях и опциях.
  5. Поиграйте пару часов с Rubular.
Листинг 6.31. Тест для кода отвечающего за перевод email в нижний регистр из Листинга 6.23.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  describe "email address with mixed case" do
    let(:mixed_case_email) { "[email protected]" }

    it "should be saved as all lower-case" do
      @user.email = mixed_case_email
      @user.save
      @user.reload.email.should == mixed_case_email.downcase
    end
  end
  .
  .
  .
end
Листинг 6.32. Альтернативная реализация before_save.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password

  before_save { email.downcase! }
  .
  .
  .
end
  1. Имя происходит от “паттерна active record”, определенного и названного в Patterns of Enterprise Application Architecture Мартина Фаулера. 
  2. Произносится “ess-cue-ell”, хотя альтернативное произношение “sequel” также возможно. 
  3. Используя адреса электронной почты в качестве имени пользователя мы открываем теоретическую возможность связи с нашими пользователями в будущем. 
  4. Не волнуйтесь о том, как объект t делает это; красота уровней абстракции (the beauty of abstraction layers) состоит в том, что мы не должны этого знать. Мы можем просто доверить объекту t делать его работу. 
  5. Официально произносится как “ess-cue-ell-ite”, тем не менее, вариант произношения “sequel-ite” также возможен. 
  6. На случай, если "2011-12-05 00:57:46" вызвало ваше любопытство - я не пишу это после полуночи; временнЫе метки записаны во Всемирном координированном времени (UTC), которое для многих практических целей является аналогом Среднего времени по Гринвичу. Из NIST Time and Frequency FAQ: Q: Почему UTC используется в качестве акронима к Coordinated Universal Time вместо CUT? A: В 1970 система Coordinated Universal Time была разработана международной консультативной группой технических экспертов в рамках International Telecommunication Union (ITU). ITU чувствовал, что было лучше определить единственное сокращение для использования на всех языках, чтобы минимизировать беспорядок. Так как единогласное соглашение не могло быть достигнуто при использовании английского порядка слов, CUT, или французского порядка слов, TUC, акроним, UTC был выбран в качестве компромисса. 
  7. Значение user.updated_at говорит вам о том что временнАя метка была в UTC. 
  8. Исключения и обработка исключений - несколько более продвинутые предметы Ruby и мы не сильно будем нуждаться в них в этой книге. Они важны, тем не менее, и я предлагаю узнать о них используя одну из книг, рекомендованных в Разделе 1.1.1
  9. Я опускаю выводы консольных команд когда они не особенно поучительны, например, результат User.new
  10. Обратите внимание, что в Таблице 6.1, “буква” на самом деле означает “строчную букву”, но i в конце regex обеспечивает нечувствительность к регистру. 
  11. Если вы считаете это столь же полезным, как я, призываю вас внести пожертвование чтобы вознаградить разработчика Michael Lovitt за его замечательную работу над Rubular. 
  12. Знаете ли вы, что "Michael Hartl"@example.com, с кавычками и пробелом в середине - является допустимым адресом электронной почты согласно стандарту? Невероятно, абсурдно. Если у вас нет адреса электронной почты, который содержит только буквы, номера, подчеркивания, и точки, то получите. N.B. regex в Листинге 6.17 также позволяет знаки "плюс", потому что Gmail (и возможно другие сервисы электронной почты) делает что-то полезное с ними: например, чтобы фильтровать email от example.com, вы можете использовать [email protected], который пойдет по Gmail адресу [email protected], позволив вам фильтровать по строке example
  13. Как было вкратце отмечено во введении в этот раздел, есть отдельная тестовая база данных, db/test.sqlite3 для этой цели. 
  14. Конечно, мы могли только отредактировать файл миграции для таблицы users в Листинге 6.2, но для этого потребовалось бы откатить, а затем вновь накатить миграцию базы данных. Rails Way заключается в использовании миграции каждый раз, когда мы обнаруживаем, что нам необходимо изменить модель данных. 
  15. Непосредственные эксперименты со SQLite на моей системе и с PostgreSQL на Heroku показали что этот шаг фактически необходим. 
  16. https://railstutorial.ru/book?version=3.0