Ruby on Rails Tutorial

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

Майкл Хартл

Содержание

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

Предисловие

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

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

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

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

Derek Sivers (sivers.org)
Основатель CD Baby

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

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

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

Об авторе

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

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

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

Лицензия MIT

Copyright (c) 2013 Michael Hartl

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

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

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

Глава 9 Обновление, демонстрация и удаление пользователей

В этой главе мы завершим REST действия для ресурса Users (Таблица 7.1) добавив edit, update, index, и destroy действия. Мы начнем с того, что дадим пользователям возможность обновлять свои профили, что также обеспечит естественную возможность для обеспечения модели безопасности (стало возможным, благодаря аутентификационному коду из Главы 8). Затем мы сделаем список всех пользователей (также требует авторизации), что будет поводом для внедрения образцов данных и постраничного вывода (пагинации). Наконец, мы также добавим возможность удалять пользователей, стиранием их в базе данных. Так как мы не можем позволить любому пользователю обладать такими опасными возможностями, мы позаботимся о создании привилегированного класса административных пользователей (администраторов), авторизованных для удаления других пользователей.

Мы начнем с создания отдельной ветки updating-users:

$ git checkout -b updating-users

9.1 Обновление пользователей

Основная идея редактирования информации о пользователе тесно параллельна созданию новых пользователей (Глава 7). Вместо new действия визуализирующего представление для нового пользователя, мы имеем edit действие, отображающее представление для редактирования пользователей; вместо create отвечающего на запрос POST, мы имеем update действие, отвечающее на запрос PATCH (Блок 3.3). Основное отличие заключается в том, что зарегистрироваться может любой человек, но только текущий пользователь должен иметь возможность обновлять свою информацию. Это означает, что нам необходимо обеспечить контроль доступа таким образом, чтобы только авторизированные пользователи могли редактировать и обновлять информацию; аутентификационный механизм из Главы 8 позволит нам использовать предфильтр для обеспечения этого вида контроля.

9.1.1 Форма для редактирования

Мы начнем с формы редактирования, набросок которой представлен на Рис. 9.1.1 Как обычно, мы начнем с тестов. Во-первых, обратите внимание на ссылку для смены изображения Gravatar; если вы зайдете на сайт Gravatar, вы увидите, что страница для добавления или редактирования изображений находится по адресу http://gravatar.com/emails, и мы протестируем наличие на странице edit этого URL.2

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

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

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

describe "User pages" do
  .
  .
  .
  describe "edit" do
    let(:user) { FactoryGirl.create(:user) }
    before { visit edit_user_path(user) }

    describe "page" do
      it { should have_content("Update your profile") }
      it { should have_title("Edit user") }
      it { should have_link('change', href: 'http://gravatar.com/emails') }
    end

    describe "with invalid information" do
      before { click_button "Save changes" }

      it { should have_content('error') }
    end
  end
end

Для того чтобы написать код приложения, нам необходимо заполнить действие edit контоллера Users. Таблица 7.1 указывает на то, что правильным URL для страницы редактирования пользователя является /users/1/edit (предполагается что id пользователя равен 1). Вспомните что id пользователя доступен в переменной params[:id], а это означает что мы можем найти пользователя с помощью кода в Листинг 9.2.

Листинг 9.2. Действие edit контроллера Users.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def edit
    @user = User.find(params[:id])
  end
  .
  .
  .
end

Для прохождения этих тестов требуется создание соответствующего (edit) представления, показанного в Листинге 9.3. Обратите внимание, как сильно оно походит на представление new user из Листинга 7.17; большое перекрытие предполагает факторинг повторяющгося кода в партиал, который мы оставим в качестве упражнения (Раздел 9.6).

Листинг 9.3. Представление user edit.
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

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

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

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

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

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

      <%= f.submit "Save changes", class: "btn btn-large btn-primary" %>
    <% end %>

    <%= gravatar_for @user %>
    <a href="http://gravatar.com/emails">change</a>
  </div>
</div>

Здесь мы повторно использовали общедоступный партиал error_messages, введеный в Разделе 7.3.3.

С переменной экземпляра @user из Листинга 9.2, тесты для страницы редактирования из Листинга 9.1 должны пройти:

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

Соответствующая страниц показана на Рис. 9.2, который показывает, как Rails автоматически предзаполняет Name и Email поля используя аттрибуты переменной @user.

edit_page_bootstrap
Рисунок 9.2: Начальная страница редактирования пользователя с предзаполненными полями Name & Email. (полный размер)

Посмотрев на исходный HTML для Рис. 9.2, как и ожидалось, мы видим тег формы (Листинг 9.4).

Листинг 9.4. HTML для формы редактирования определенной в Листинге 9.3 и показанной на Рис. 9.2.
<form action="/users/1" class="edit_user" id="edit_user_1" method="post">
  <input name="_method" type="hidden" value="patch" />
  .
  .
  .
</form>

Обратите внимание на скрытое поле ввода

<input name="_method" type="hidden" value="patch" />

Поскольку веб-браузеры сами по себе не могут отправлять PATCH запросы (как это требует от них REST конвенция из Таблицы 7.1), Rails подделывает иx с помощью POST запроса и скрытого поля input.3

Стоит также упомянуть здесь еще одну тонкость: код form_for(@user) в Листинге 9.3 абсолютно совпадает с кодом в Листинге 7.17 — так как же Rails узнает, что нужно использовать POST запрос для новых пользователей и PATCH для редактирования уже существующих? Ответ кроется в возможности определения того, что мы имеем дело с новым или уже существующим в базе данных пользователем, посредством булевого метода new_record? библиотеки Active Record:

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

При построении формы с помощью form_for(@user), Rails использует POST если @user.new_record? это true и PATCH если это является false.

В качестве финального штриха мы добавим URL к ссылке на настройки пользователя в навигации сайта. Поскольку она зависит от статуса вошедшего, тест для ссылки “Settings” относится к остальным тестам аутентификации, как это показано в Листинге 9.5. (Было бы неплохо иметь дополнительные тесты которые проверяли бы что подобные ссылки не видны невошедшим пользователям; написание этих тестов остается в качестве упражнения (Раздел 9.6).)

Листинг 9.5. Добавление теста для ссылки “Settings”.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
    .
    .
    .
    describe "with valid information" do
      let(:user) { FactoryGirl.create(:user) }
      before { sign_in user }

      it { should have_title(user.name) }
      it { should have_link('Profile',     href: user_path(user)) }
      it { should have_link('Settings',    href: edit_user_path(user)) }
      it { should have_link('Sign out',    href: signout_path) }
      it { should_not have_link('Sign in', href: signin_path) }
      .
      .
      .
    end
  end
end

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

Листинг 9.6. Тестовый хелпер для входа пользователей.
spec/support/utilities.rb
.
.
.
def sign_in(user, options={})
  if options[:no_capybara]
    # Sign in when not using Capybara.
    remember_token = User.new_remember_token
    cookies[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
  else
    visit signin_path
    fill_in "Email",    with: user.email
    fill_in "Password", with: user.password
    click_button "Sign in"
  end
end

Как было отмечено в комментарии, заполнение формы не будет работать в отсутствие Capybara и для того чтобы покрыть этот случай мы позволяем пользователю передать опцию no_capyabara: true для переписывания дефолтного метода входа и непосредственной манипуляции куками. Это нобходимо при использовании одного из методов HTTP запроса напрямую (get, post, patch или delete), как мы увидим в Листинге 9.45. (Обратите внимание: тестовый объект cookies - не самая замечательная симуляция реальных куки; в частности, метод cookies.permanent который мы видели в Листинге 8.19 не работает внутри тестов.) Как вы могли ожидать, метод sign_in пригодится в будущих тестах и фактически он уже может быть использован для устранения нескольких повторений (Раздел 9.6).

Код приложения необходимый для добавления URL к ссылке “Settings” прост: мы применим именованный маршрут edit_user_path из Таблицы 7.1, совместно с удобным вспомогательным методом current_user определенным в Листинге 8.22:

<%= link_to "Settings", edit_user_path(current_user) %>

Полный код приложения представлен в Листинге 9.7).

Листинг 9.7. Добавление ссылки “Settings”.
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", root_path, id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home", root_path %></li>
          <li><%= link_to "Help", help_path %></li>
          <% if signed_in? %>
            <li><%= link_to "Users", '#' %></li>
            <li id="fat-menu" class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Account <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li><%= link_to "Profile", current_user %></li>
                <li><%= link_to "Settings", edit_user_path(current_user) %></li>
                <li class="divider"></li>
                <li>
                  <%= link_to "Sign out", signout_path, method: "delete" %>
                </li>
              </ul>
            </li>
          <% else %>
            <li><%= link_to "Sign in", signin_path %></li>
          <% end %>
        </ul>
      </nav>
    </div>
  </div>
</header>

9.1.2 Провальное редактирование

В этом разделе мы обработаем случай провального редактирования и получим прохождение теста сообщения об ошибке Листинг 9.1. Код приложения создает действие update которое использует update_attributes (Раздел 6.1.5) для обновления пользователя на основе отправленного хэша params, как это показано в Листинге 9.8. С невалидной информацией, попытка обновления вернет false и ветка else заново отрендерит страницу редактирования. Мы видели этот способ ранее; структура очень похожа на первую версию действия create (Листинг 7.21).

Листинг 9.8. Начальное действие update контроллера Users.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # Handle a successful update.
    else
      render 'edit'
    end
  end
  .
  .
  .
end

Обратите внимание на использование user_params в вызове update_attributes, который использует строгие параметры для предотвращения уязвимости массового назначения (как было описано в Разделе 7.3.2).

Получившееся в результате сообщение об ошибке (Рис. 9.3) - именно то что нам нужно для прохождения теста, что вам следует проверить запустив набор тестов:

$ bundle exec rspec spec/
edit_with_invalid_information_bootstrap
Рисунок 9.3: Сообщение об ошибке после отправления формы обновления. (полный размер)

9.1.3 Успешное редактирование

Теперь пришло время заставить работать форму редактирования. Редактирование профильных изображений уже работает поскольку мы переложили загрузку изображений на Gravatar; мы можем отредактировать граватар, кликнув по ссылке “change”, показанной на Рис. 9.2, как это показано на Рис. 9.4. Давайте заставим работать остальной функционал редактирования пользователя.

gravatar_cropper
Рисунок 9.4: Интерфейс обрезки изображений Gravatar с фоткой какого-то чувака.

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

Листинг 9.9. Тесты для update действия контроллера Users.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "edit" do
    let(:user) { FactoryGirl.create(:user) }
    before do
      sign_in user
      visit edit_user_path(user)
    end
    .
    .
    .
    describe "with valid information" do
      let(:new_name)  { "New Name" }
      let(:new_email) { "new@example.com" }
      before do
        fill_in "Name",             with: new_name
        fill_in "Email",            with: new_email
        fill_in "Password",         with: user.password
        fill_in "Confirm Password", with: user.password
        click_button "Save changes"
      end

      it { should have_title(new_name) }
      it { should have_selector('div.alert.alert-success') }
      it { should have_link('Sign out', href: signout_path) }
      specify { expect(user.reload.name).to  eq new_name }
      specify { expect(user.reload.email).to eq new_email }
    end
  end
end

Обратите внимание на то что Листинг 9.9 добавляет метод sign_in из Листинга 9.6 в блок before, что необходимо для прохождения тестов ссылки “Sign out” и также предполагает защиту действия edit от невошедших пользователей (Раздел 9.2.1).

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

specify { expect(user.reload.name).to  eq new_name }
specify { expect(user.reload.email).to eq new_email }

Этот код перезагружает user переменную из (тестовой) базы данных используя user.reload, а затем проверяет, что новые имя пользователя и email совпадают с новыми значениями.

Действие update, необходимое для прохождения тестов в Листинге 9.9 аналогично финальной форме create действия (Листинг 8.27), как видно в Листинге 9.10. Единственное что он делает, это добавляет

flash[:success] = "Profile updated"
redirect_to @user

к коду в Листинге 9.8.

Листинг 9.10. Дейcтвие update контроллера Users.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
end

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

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

$ bundle exec rspec spec/

9.2 Авторизация

Одним из приятных эффектов построения механизма аутентификации в Главе 8 является то, что мы теперь готовы к реализации авторизации: аутентификация позволяет нам идентифицировать пользователей нашего сайта, а авторизация позволяет нам контролировать предоставляемые им возможности.

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

signin_page_protected_mockup_bootstrap
Рисунок 9.5: Набросок результата посещения защищенной страницы (полный размер)

9.2.1 Требование входа пользователей

Поскольку ограничения безопасности для edit и update действий идентичны, мы будем обрабатывать их в одном RSpec describe блоке. Начав с требования входа, наши первоначальные тесты затем проверяют, что не вошедшие пользователи, пытающиеся получить доступ к какому либо из действий, просто перенаправляеются на страницу входа, как показано в Листинге 9.11.

Листинг 9.11. Тестирование того, что edit и update действия защищены.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }

      describe "in the Users controller" do

        describe "visiting the edit page" do
          before { visit edit_user_path(user) }
          it { should have_title('Sign in') }
        end

        describe "submitting to the update action" do
          before { patch user_path(user) }
          specify { expect(response).to redirect_to(signin_path) }
        end
      end
    end
  end
end

Код в Листинге 9.11 вводит второй способ, отличающийся от метода visit предоставляемого Capybara, для доступа к действию контроллера: выдавая соответствующий HTTP запрос непосредственно, в данном случае, с помощью метода patch для выдачи запроса PATCH:

describe "submitting to the update action" do
  before { patch user_path(user) }
  specify { expect(response).to redirect_to(signin_path) }
end

Это выдает запрос PATCH непосредственно к /users/1, который направляет к update действию контроллера Users (Таблица 7.1). Это необходимо из-за того, что браузер не может посетить непосредственно само действие update — он может лишь попасть туда через отправку формы редактирования — так что Capybara тоже не может этого сделать. Но посещение страницы редактирования тестирует только авторизацию для действия edit, но не для update. В результате, единственный способ как следует протестировать авторизацию для самого действия update это выдать непосредственный запрос. (Как вы можете догадаться, в дополнение к patch Rails тесты поддерживают также get, post, и delete.)

При использовании одного из способов непосредственной выдачи HTTP запросов, мы получаем доступ к низкоуровневому объекту response. В отличие от объекта Capybara page, response позволяет нам тестировать сам ответ сервера, в данном случае, проверяя что действие update отвечает переадресацией на страницу входа:

specify { expect(response).to redirect_to(signin_path) }

Авторизационный код приложения использует предфильтр, который использует before_action команду для указания конкретному методу быть вызванным до данных действий. (Раньше команда для предфильтров называлась before_filter, но ядро разработчиков Rails решило переименовать его для того чтобы подчеркнуть что фильтр исполняется перед заданным действием контроллера.) Для того чтобы требовать от пользователей входа, мы определяем signed_in_user метод и вызываем его с помощью before_action :signed_in_user, как это показано в Листинге 9.12.

Листинг 9.12. Добавление предфильтра signed_in_user.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update]
  .
  .
  .
  private

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

    # Before filters

    def signed_in_user
      redirect_to signin_url, notice: "Please sign in." unless signed_in?
    end
end

По умолчанию, предфильтры применяются ко всем действиям контроллера, поэтому мы ограничиваем действие фильтра только :edit и :update действиями, посредством передачи соответствующего хэша опций :only.

Обратите внимание, что Листинг 9.12 использует сокращение для установки flash[:notice] передавая хэш опций в функцию redirect_to. Код в Листинге 9.12 эквивалентен более многословному

unless signed_in?
  flash[:notice] = "Please sign in."
  redirect_to signin_url
end

(К сожалению данная конструкция не работает для ключей :error и :success.)

Совместно с :success и :error, ключ :notice завершает наш триумвират отстиленных flash, поддерживаемых Bootstrap CSS фреймворком. Выйдя из сайта и попытавшись получить доступ к странице редактирования пользователя /users/1/edit, мы можем увидеть результирующий желтый блок “notice”, как это показано на Рис. 9.6.

protected_sign_in_bootstrap
Рисунок 9.6: Форма входа после попытки получить доступ к защищенной странице. (полный размер)

В этой точке наш набор тестов должен решительно позеленеть:

$ bundle exec rspec spec/

9.2.2 Требование правильного пользователя

Конечно, требования входа пользователей недостаточно; пользователи должны иметь доступ к редактированию только своей информации. Мы можем протестировать это, вначале войдя как неправильный пользователь, а затем обратившись к edit и update действиям (Листинг 9.13). Обратите внимание: поскольку мы не используем Capybara для этих тестов (no_capybara: true), мы используем методы get и patch для обращения к действиям edit и update напрямую. К тому же, так как пользователи никогда не должны даже пытаться изменить профиль другого пользователя, мы сделаем переадресацию не на страницу входа, а на корневой URL.

Листинг 9.13. Тестирование того, что действия edit и update требуют правильного пользователя.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do
    .
    .
    .
    describe "as wrong user" do
      let(:user) { FactoryGirl.create(:user) }
      let(:wrong_user) { FactoryGirl.create(:user, email: "wrong@example.com") }
      before { sign_in user, no_capybara: true }

      describe "submitting a GET request to the Users#edit action" do
        before { get edit_user_path(wrong_user) }
        specify { expect(response.body).not_to match(full_title('Edit user')) }
        specify { expect(response).to redirect_to(root_url) }
      end

      describe "submitting a PATCH request to the Users#update action" do
        before { patch user_path(wrong_user) }
        specify { expect(response).to redirect_to(root_url) }
      end
    end
  end
end

Обратите внимание на то, что фабрика может принимать опцию:

FactoryGirl.create(:user, email: "wrong@example.com")

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

Код приложения добавляет второй предфильтр для вызова метода correct_user, как это показано в Листинге 9.14.

Листинг 9.14. Предфильтр correct_user для защиты edit/update pages.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private

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

    # Before filters

    def signed_in_user
      redirect_to signin_url, notice: "Please sign in." unless signed_in?
    end

    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

Фильтр correct_user использует булевый метод current_user?, который мы определили в хелпере Sessions (Листинг 9.15).

Листинг 9.15. Метод current_user?.
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

  def current_user?(user)
    user == current_user
  end
  .
  .
  .
end

Листинг 9.14 также показывает обновленные edit и update действия. Ранее, в Листинге 9.2 мы имели

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

и аналогично для update. Но теперь, когда предфильтр correct_user определяет @user, мы можем опустить это для обоих действий.

Прежде чем двигаться дальше, вам следует проверить что набор тестов проходит:

$ bundle exec rspec spec/

9.2.3 Дружелюбная переадресация

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

Для того чтобы протестировать такую “дружелюбную переадресацию”, мы вначале посещаем страницу редактирования пользователя, которая перенаправит нас на страницу входа. Затем мы введем валидную информацию для входа и кликнем по кнопке “Sign in”. Результирующей страницей, которой по дефолту является профиль пользователя, в данном случае должна быть страница “Edit user”. Тест для этой последовательности представлен в Листинге 9.16.

Листинг 9.16. Тест для дружелюбной переадресации.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }

      describe "when attempting to visit a protected page" do
        before do
          visit edit_user_path(user)
          fill_in "Email",    with: user.email
          fill_in "Password", with: user.password
          click_button "Sign in"
        end

        describe "after signing in" do

          it "should render the desired protected page" do
            expect(page).to have_title('Edit user')
          end
        end
      end
      .
      .
      .
    end
    .
    .
    .
  end
end

Теперь реализация.4 Для того чтобы перенаправить пользователей к запрашиваемой ими странице, нам нужно где-то сохранить запрашиваемую страницу, а затем переадресовать к ней. Мы добьемся этого с помощью пары методов, store_location и redirect_back_or, определенных в хелпере Sessions (Листинг 9.17).

Листинг 9.17. Код реализующий дружественную переадресацию.
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def redirect_back_or(default)
    redirect_to(session[:return_to] || default)
    session.delete(:return_to)
  end

  def store_location
    session[:return_to] = request.url if request.get?
  end
end

В роли механизма хранения выступает объект session предоставляемый Rails, о котором вы можете думать как об экземпляре переменной cookies из Раздела 8.2.1 которая автоматически истекает при закрытии браузера. Мы также используем объект request для получения url, т.e. URL запрашиваемой страницы. Метод store_location помещает запрашиваемый URL в переменную session под ключом :return_to, но только для GET запроса (if request.get?). Это предотвращает сохранение URL для перенаправления если пользовател, скажем, отправляет форму не будучи залогиненым (что, пусть и является крайним случаем, но все же может случиться если, например, пользователь удалил remember token вручную перед отправкой формы); в данном случае, результирующий редирект выдаст GET запрос к URL ожидающему POST, PATCH или DELETE, что приведет к ошибке.5

Для того чтобы использовать store_location, нам необходимо добавить ее в предфильтр signed_in_user, как это показано в Листинге 9.18.

Листинг 9.18. Добавление store_location в предфильтр :signed_in_user.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end
  .
  .
  .
  private

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

    # Before filters

    def signed_in_user
      unless signed_in?
        store_location
        redirect_to signin_url, notice: "Please sign in."
      end
    end

    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

Для того чтобы реализовать саму переадресацию, мы используем метод redirect_back_or для перенаправления на запрашиваемый URL если он существует или на какой-либо дефолтный URL в противном случае, который мы добавим в действие create контроллера Sessions для переадресации после успешного входа (Листинг 9.19). Метод redirect_back_or использует оператор "или" ||

session[:return_to] || default

Этот код оценивает session[:return_to] и до тех пор, пока оно не является nil, в противном случае он оценивает заданный дефолтный URL. Обратите внимание, что Листинг 9.17 заботится об удалении URL перенаправления; в противном случае, последующие попытки входа перенаправлялись бы на защищенную страницу до тех пор, пока пользователь не закроет браузер. (Тестирование этого поведения оставлено в качестве упражнения (Раздел 9.6.)

Листинг 9.19. Действие create контроллера Sessions с дружелюбной переадресацией.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      sign_in user
      redirect_back_or user
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end

(Если вы выполнили первое упражнение в Главае 8, убедитесь в том что вы используете правильный хэш params в Листинге 9.19.)

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

$ bundle exec rspec spec/

9.3 Отображение всех пользователей

В этом разделе мы добавим предпоследнее действие пользователя, действие index, которое предназначено для отображения всех пользователей вместо одного единственного. По дороге мы узнаем о заполнении базы данных образцами ползователей и пагинации вывода пользователей с тем, чтобы страница со списком пользователей могла масштабироваться для отображения потенциально большого количества пользователей. Набросок результата — пользователи, пагинационные ссылки и навигационная ссылка “Users” — представлена на Рис. 9.7.6 В Разделе 9.4 мы добавим административный интерфейс к списку пользователей для того чтобы (предположительно проблемные) могли быть удалены.

user_index_mockup_bootstrap
Рисунок 9.7: Набросок списка пользователей с пагинацией и навигационной ссылкой “Users”. (полный размер)

9.3.1 Список пользователей

Хотя мы сохраним страницы show отдельных пользователей видимыми для всех посетителей сайта, для страницы user index будет реализовано ограничение, отображающее ее только для зарегистрированных пользователей, также будет реализовано ограничение того, сколько зарегистрированных пользователей будет отображаться на каждой странице списка. Мы начнем с тестирования того, что действие index защищено, посетив users_path (Таблица 7.1) и проверив что мы перенаправлены на страницу входа. Как и с остальными тестами авторизации, мы поместим этот пример в интеграционные тест авторизации, как это показано в Листинге 9.20.

Листинг 9.20. Тестирование того, что действие index защищено.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      .
      .
      .
      describe "in the Users controller" do
        .
        .
        .
        describe "visiting the user index" do
          before { visit users_path }
          it { should have_title('Sign in') }
        end
      end
      .
      .
      .
    end
  end
end

Соответствующий код приложения просто добавляет index в список действий защищенных предфильтром signed_in_user, как это показано в Листинге 9.21.

Листинг 9.21. Требование входа пользователя для действия index.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  def index
  end

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

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

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

describe "User pages" do

  subject { page }

  describe "index" do
    before do
      sign_in FactoryGirl.create(:user)
      FactoryGirl.create(:user, name: "Bob", email: "bob@example.com")
      FactoryGirl.create(:user, name: "Ben", email: "ben@example.com")
      visit users_path
    end

    it { should have_title('All users') }
    it { should have_content('All users') }

    it "should list each user" do
      User.all.each do |user|
        expect(page).to have_selector('li', text: user.name)
      end
    end
  end
  .
  .
  .
end

Как вы можете вспомнить из соответствующего действия в 'demo app' (Листинг 2.4), код приложения использует User.all для вытягивания всех пользователей из базы данных, присваивая их переменной экземпляра @users для использования в представлении, как это показано в Листинге 9.23. (Если отображение всех пользователей за раз кажется вам плохой идеей, вы правы и мы избавимся от этого недостатка в Разделе 9.3.3.)

Листинг 9.23. Действие index контроллера Users.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

Для того, чтобы на самом деле создать страницу, нам необходимо создать представление, которое перебирает всех пользователей и обертывает каждого из них в тег li. Мы сделаем это с помощью метода each, отображающего Gravatar и имя каждого пользователя, в то время как сам он будет завернут в тег ненумерованного списка (ul) (Листинг 9.24). Код в Листинге 9.24 использует результат Листинга 7.30 из Раздела 7.6, который позволяет нам передать опцию, определяющую размер отличный от дефолтного, в хелпер Gravatar. Если вы не выполнили это упражнение, обновите ваш файл хелпера Users с содержимым Листинга 7.30 прежде чем продолжать.

Листинг 9.24. Представление для страницы со списком пользователей.
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 52 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

Давайте также добавим немного CSS (или, скорее, SCSS) для придания стиля (Листинг 9.25).

Листинг 9.25. CSS для страницы со списком пользователей.
app/assets/stylesheets/custom.css.scss
.
.
.

/* Users index */

.users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-top: 1px solid $grayLighter;
    &:last-child {
      border-bottom: 1px solid $grayLighter;
    }
  }
}

Наконец, мы добавим URL в ссылку на список пользователей в навигационном меню шапки сайта с помощью users_path, тем самым применив последний из неиспользованных именованных маршрутов Таблицы 7.1. Тест (Листинг 9.26) и код приложения (Листинг 9.27) довольно просты.

Листинг 9.26. Тест для URL ссылки “Users”.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
    .
    .
    .
    describe "with valid information" do
      let(:user) { FactoryGirl.create(:user) }
      before { sign_in user }

      it { should have_title(user.name) }
      it { should have_link('Users',       href: users_path) }
      it { should have_link('Profile',     href: user_path(user)) }
      it { should have_link('Settings',    href: edit_user_path(user)) }
      it { should have_link('Sign out',    href: signout_path) }
      it { should_not have_link('Sign in', href: signin_path) }
      .
      .
      .
    end
  end
end
Листинг 9.27. Добавление URL к ссылке на список пользователей.
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", root_path, id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home", root_path %></li>
          <li><%= link_to "Help", help_path %></li>
          <% if signed_in? %>
            <li><%= link_to "Users", users_path %></li>
            <li id="fat-menu" class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Account <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li><%= link_to "Profile", current_user %></li>
                <li><%= link_to "Settings", edit_user_path(current_user) %></li>
                <li class="divider"></li>
                <li>
                  <%= link_to "Sign out", signout_path, method: "delete" %>
                </li>
              </ul>
            </li>
          <% else %>
            <li><%= link_to "Sign in", signin_path %></li>
          <% end %>
        </ul>
      </nav>
    </div>
  </div>
</header>

С этим кодом список пользователей стал полностью функциональным, и все тесты должны проходить:

$ bundle exec rspec spec/

Но с другой стороны, как это видно на Рис. 9.8, он выглядит несколько безлюдно. Давайте исправим эту печальную ситуацию.

user_index_only_one_bootstrap
Рисунок 9.8: Страница списка пользователей /users с одним пользователем. (полный размер)

9.3.2 Образцы пользователей

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

Во-первых, мы добавим Faker гем в Gemfile, который позволит нам делать образцы пользователей с полу-реалистичными именами и адресами электронной почты (Листинг 9.28).

Листинг 9.28. Добавление Faker гема в Gemfile.
source 'https://rubygems.org'
ruby '2.0.0'
#ruby-gemset=railstutorial_rails_4_0

gem 'rails', '4.0.2'
gem 'bootstrap-sass', '2.3.2.0'
gem 'bcrypt-ruby', '3.1.2'
gem 'faker', '1.1.2'
.
.
.

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

$ bundle install

Далее мы добавим Rake-задачу для создания образцов пользователей. Rake задачи живут в lib/tasks, и определяются с помощью пространства имен (в даном случае, :db), как видно в Листинге 9.29. (Это довольно продвинутый материал, так что не особо заморачивайтесь деталями.)

Листинг 9.29. Rake задача для заполнения базы данных образцами пользователей.
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    User.create!(name: "Example User",
                 email: "example@railstutorial.org",
                 password: "foobar",
                 password_confirmation: "foobar")
    99.times do |n|
      name  = Faker::Name.name
      email = "example-#{n+1}@railstutorial.org"
      password  = "password"
      User.create!(name: name,
                   email: email,
                   password: password,
                   password_confirmation: password)
    end
  end
end

Этот код определяет задачу db:populate которая создает образец пользователя с именем и адресом электронной почты, делая реплику нашего предыдущего пользователя, после чего делает еще 99 экземпляров. Строка

task populate: :environment do

обеспечивает Rake задаче доступ к локальному Rails окружению, включая модель User (и, следовательно, к User.create!). Здесь create! это метод очень похожий на create, за той лишь разницей, что он вызывает исключение (Раздел 6.1.4) при неудачном создании, вместо того чтобы тихо возвращать false. Эта крикливая конструкция упрощает отладку, помогая избежать тихих ошибок.

С пространством имен :db как в Листинге 9.29, мы можем вызвать Rake задачу следующим образом:

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

После запуска Rake задачи, наше приложениее имеет 100 примеров пользователей, как это видно на Рис. 9.9. (Я взял на себя смелость связать первые несколько образцов адресов с фотографиями так что не все изображения являются дефолтными картинками Gravatar.)

user_index_all_bootstrap
Рисунок 9.9: Страница списка пользователей /users с 100 образцов пользователей. (полный размер)

9.3.3 Пагинация

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

В Rails есть несколько способов разбиения на страницы, мы будем использовать один из самых простых и надежных, он называется will_paginate. Для того, чтобы использовать его нам необходимо включить сам гем will_paginate, а также гем bootstrap-will_paginate, который конфигурирует will_paginate для использования пагинационных стилей предоставляемых Bootstrap. Обновленный Gemfile представлен в Листинге 9.30.

Листинг 9.30. Включение will_paginate в Gemfile.
source 'https://rubygems.org'
ruby '2.0.0'
#ruby-gemset=railstutorial_rails_4_0

gem 'rails', '4.0.2'
gem 'bootstrap-sass', '2.3.2.0'
gem 'bcrypt-ruby', '3.1.2'
gem 'faker', '1.1.2'
gem 'will_paginate', '3.0.4'
gem 'bootstrap-will_paginate', '0.0.9'
.
.
.

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

$ bundle install

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

Поскольку гем will_paginate широко распространен, нам нет необходимости тестировать его, так что мы можем применить несколько облегченный подход. Во-первых, мы протестируем наличие div с CSS классом “pagination”, который выводится гемом will_paginate. Затем мы проверим что на первой странице результатов представлены правильные пользователи. Для этого нам потребуется использовать метод paginate, о котором мы вскоре узнаем подробнее.

Как и прежде, мы будем использовать Factory Girl для имитации пользователей, но мы тут же натыкаемся на проблему: адреса электронной почты пользователей должны быть уникальными, что, как представляется, требует создания более чем 30 пользователей вручную — ужасно муторная работа. К тому же, при тестировании выводимого списка пользователей было бы удобно, если бы у них были разные имена. К счастью, Factory Girl предвидела этот вопрос, и обеспечила последовательность (цикл) для ее решения. Наша оригинальная фабрика (Листинг 7.8) хард-кодила имя и адрес электронной почты:

FactoryGirl.define do
  factory :user do
    name     "Michael Hartl"
    email    "michael@example.com"
    password "foobar"
    password_confirmation "foobar"
  end
end

Вместо этого мы можем организовать последовательность имен и адресов электронной почты с помощью метода sequence:

factory :user do
  sequence(:name)  { |n| "Person #{n}" }
  sequence(:email) { |n| "person_#{n}@example.com"}
  .
  .
  .

Здесь sequence принимает символ соответствующий выбранному атрибуту (такому как :name) и блок с одной переменной, которую мы назвали n. При последующих вызовах FactoryGirl метод,

FactoryGirl.create(:user)

Переменная блока n автоматически увеличивается, таким образом, именем первого пользователя будет “Person 1”, а адресом электронной почты - “person_1@example.com”, второй пользователь получит имя “Person 2” и адрес электронной почты “person_2@example.com”, и т.д.. Полный код представлен в Листинге 9.31.

Листинг 9.31. Определение последовательности (цикла) Factory Girl.
spec/factories.rb
FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}
    password "foobar"
    password_confirmation "foobar"
  end
end

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

before(:all) { 30.times { FactoryGirl.create(:user) } }
after(:all)  { User.delete_all }

Обратите здесь внимание на применение before(:all), который гарантирует создание образцовых пользователей единожды перед всеми тестами блока. Это оптимизирует скорость прохождения тестов, поскольку создание 30 пользователей может быть медленным на некоторых системах. Мы используем тесно связанный метод after(:all) для удаления прользователей по завершении.

Тесты на появление пагинационного div и наличие правильных пользователей представлены в Листинге 9.32. Обратите внимание на замену массива User.all из Листинга 9.22 на User.paginate(page: 1), который (как мы вскоре увидим) вытягивает первую страницу пользователей из базы данных. Обратите также внимание на то, что Листинг 9.32 использует before(:each) для того чтобы подчеркнуть контраст с before(:all).

Листинг 9.32. Тесты для пагинации.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }

  describe "index" do
    let(:user) { FactoryGirl.create(:user) }
    before(:each) do
      sign_in user
      visit users_path
    end

    it { should have_title('All users') }
    it { should have_content('All users') }

    describe "pagination" do

      before(:all) { 30.times { FactoryGirl.create(:user) } }
      after(:all)  { User.delete_all }

      it { should have_selector('div.pagination') }

      it "should list each user" do
        User.paginate(page: 1).each do |user|
          expect(page).to have_selector('li', text: user.name)
        end
      end
    end
  end
  .
  .
  .
end

Для того чтобы подключить пагинацию нам нужно добавить немного кода сообщающего Rails о необходимости пагинировать пользователей в представлении index и нам необходимо заменить User.all в index действии на объект который знает о пагинации. Мы начнем с добавления специального метода will_paginate в представление (Листинг 9.33); мы вскоре увидим почему код был добавлен сверху и снизу списка пользователей.

Листинг 9.33. Страница списка пользователей с пагинацией.
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 52 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

Метод will_paginate немного волшебный; внутри представления users он автоматически ищет объект @users, а затем отображает пагинационные ссылки для доступа к остальным страницам. Представление в Листинге 9.33 пока не работает - из-за того что на данный момент @users содержит результаты User.all (Листинг 9.23), в то время как will_paginate требует чтобы мы пагинировали результаты явно используя метод paginate:

$ rails console
>> User.paginate(page: 1)
  User Load (1.5ms)  SELECT "users".* FROM "users" LIMIT 30 OFFSET 0
   (1.7ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...

Обратите внимание, что paginate принимает в качестве аргумента хэш с ключом :page и значением, равным запрашиваемой странице. User.paginate вытягивает пользователей из базы данных по одному куску за раз (30 по умолчанию), основываясь на параметре :page. Так, например, стр. 1 содержит пользователей с 1 по 30, стр. 2 это пользователи 31–60, и т.д.. Если страница является nil, paginate просто возвращает первую страницу.

Мы можем разбить список пользователей на страницы используя paginate вместо all в index действии (Листинг 9.34). Здесь :page параметр приходит из params[:page] который will_paginate сгенерировал автоматически.

Листинг 9.34. Пагинация пользователей в index действии.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.paginate(page: params[:page])
  end
  .
  .
  .
end

Страница списка пользователей теперь должна работать так как это показано на Рис. 9.10. (На некоторых системах в этой точке может потребоваться перезапуск Rails сервера.) Так как мы включили will_paginate сверху и снизу списка пользователей, ссылки на страницы появились в обоих местах.

user_index_pagination_rails_3_bootstrap
Рисунок 9.10: Страница списка пользователей /users с пагинацией. (полный размер)

Если теперь кликнуть по любой 2 или Next ссылке, вы получите вторую страницу с результатами, как это показано на Рис. 9.11.

user_index_page_two_rails_3_bootstrap
Рисунок 9.11: Страница 2 списка пользователей (/users?page=2). (полный размер)

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

$ bundle exec rspec spec/

9.3.4 Частичный рефакторинг

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

Первым шагом нашего рефакторинга будет замена пользовательского li из Листинга 9.33 на вызов render (Листинг 9.35).

Листинг 9.35. Первая попытка рефакторинга в index представлении.
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %>
  <% end %>
</ul>

<%= will_paginate %>

Здесь мы вызываем render не на строку с именем партиала, а на перемнную user класса User;7 в этом контексте, Rails автоматически ищет партиал с названием _user.html.erb, который мы должны создать (Листинг 9.36).

Листинг 9.36. Партиал для отображения отдельно взятого пользователя.
app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 52 %>
  <%= link_to user.name, user %>
</li>

Это явное улучшение, но мы можем сделать еще лучше: мы можем вызвать render непосредственно на переменную @users (Листинг 9.37).

Листинг 9.37. Полностью реорганизованный список пользователей.
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <%= render @users %>
</ul>

<%= will_paginate %>

Здесь Rails делает вывод, что @users это список объектов User; кроме того, при вызове с коллекцией пользователей, Rails автоматически перебирает их и отображает каждого из них с помощью партиала _user.html.erb. Результатом служит впечатляюще компактный код Листинга 9.37. Как и при любом рефакторинге, вам следует проверить что набор тестов по-прежнему зеленый, несмотря на изменившийся код приложения:

$ bundle exec rspec spec/

9.4 Уничтожение пользователей

Теперь, когда список пользователей завершен, осталось лишь одно каноничное REST действие: destroy. В этом разделе мы добавим ссылки для удаления пользователей, как это показано на Рис. 9.12, и определим destroy действие необходимое для выполнения удаления. Но мы начнем с создания класса уполномоченных на это административных пользователей.

user_index_delete_links_mockup_bootstrap
Рисунок 9.12: Набросок списка пользователей с удаляющими ссылками. (полный размер)

9.4.1 Административные пользователи

Мы будем идентифицировать привилегированных пользователей с правами администратора посредством булевого атрибута admin в модели User, что, как мы увидим, автоматически приведет нас к методу admin? для проверки административного статуса. Мы можем написать тесты для этого атрибута, как в Листинге 9.38.

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

describe User do
  .
  .
  .
  it { should respond_to(:authenticate) }
  it { should respond_to(:admin) }

  it { should be_valid }
  it { should_not be_admin }

  describe "with admin attribute set to 'true'" do
    before do
      @user.save!
      @user.toggle!(:admin)
    end

    it { should be_admin }
  end
  .
  .
  .
end

Здесь мы использовали метод toggle! для изменения атрибута admin от false к true. Отметим также, что строка

it { should be_admin }

подразумевает (через булеву конвенцию RSpec), что пользователь должен иметь булев метод admin?.

Мы добавим атрибут admin как обычно, посредством миграции, указав тип boolean в командной строке:

$ rails generate migration add_admin_to_users admin:boolean

Миграция просто добавляет столбец admin к таблице users (Листинг 9.39), приводя к модели данных показанной на Рис. 9.13.

Листинг 9.39. Миграция для добавления булевого атрибута admin к пользователям.
db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

Обратите внимание на то, что мы добавили аргумент default: false к add_column в Листинге 9.39, что означает, что пользователи не будут администраторами по умолчанию. (Без аргумента default: false, admin был бы по умолчанию nil, что все же является false, так что этот шаг не является строго обязательным. Однако это более явно и четко сообщает о наших намерениях, и Rails, и читателям нашего кода.)

user_model_admin_31
Рисунок 9.13: Модель User с добавленным булевым атрибутом admin.

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

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

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

$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true

В результате, тесты для admin должны пройти:

$ bundle exec rspec spec/models/user_spec.rb

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

Листинг 9.40. Код заполнителя образцов данных для создания административного пользователя.
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    admin = User.create!(name: "Example User",
                         email: "example@railstutorial.org",
                         password: "foobar",
                         password_confirmation: "foobar",
                         admin: true)
    .
    .
    .
  end
end

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

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

Возвращение к строгим параметрам

Вы могли заметить, что Листинг 9.40 делает пользователя администратором с помощью добавления admin: true к хэшу инициализации. Это подчеркивает опасность демонстрации наших объектов в диком Вебе: если мы просто передадим инициализационный хэш с произвольного веб-запроса, вредоносный пользователь может в таком случае отправить PATCH запрос следующим образом:8

patch /users/17?admin=1

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

Из-за этой опасности очень важно передавать на дальнейшую обработку только параметры безопасных для редактирования атрибутов. Как отмечалось в Разделе 7.3.2, это может быть достигнуто с помощью строгих параметров вызывая require и permit на хэше params:

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

В частности, обратите внимание на то, что admin не включен в список разрешенных атрибутов. Именно это не позволяет произвольному пользователю предоставлять себе административный доступ к нашему приложению. Поскольку это очень важный нюанс, хорошей идеей будет написание теста на каждый нередактируемый атрибут и написание такого теста для атрибута admin оставлено в качестве упражнения (Раздел 9.6).

9.4.2 Destroy действие

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

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

Листинг 9.41. Добавление фабрики для административных пользователей.
spec/factories.rb
FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}
    password "foobar"
    password_confirmation "foobar"

    factory :admin do
      admin true
    end
  end
end

С кодом в Листинге 9.41, мы теперь можем использовать FactoryGirl.create(:admin) для создания административных пользователей в наших тестах.

Наша политика безопасности требует запретить обычным пользователям видеть удаляющие ссылки:

it { should_not have_link('delete') }

Но административные пользователи должны видеть такие ссылки и мы ожидаем, что админ, кликнув по удаляющей ссылке удалит пользователя, т.e., изменит количество User на -1:

it { should have_link('delete', href: user_path(User.first)) }
it "should be able to delete another user" do
  expect do
    click_link('delete', match: :first)
  end.to change(User, :count).by(-1)
end
it { should_not have_link('delete', href: user_path(admin)) }

Это включает код match: :first, который говорит Capybara что нам не важно какую именно удаляющую ссылку она (Капибара) кликает; это должен быть просто клик по первой из тех что она видит. Обратите внимание что мы также добавили тест для проверки того что административный пользователь не видит ссылки на удаление самого себя. Полный набор тестов для удаляющих ссылок представлен в Листинге 9.42.

Листинг 9.42. Тесты для удаляющих ссылок.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }

  describe "index" do

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

    before do
      sign_in user
      visit users_path
    end

    it { should have_title('All users') }
    it { should have_content('All users') }

    describe "pagination" do
      .
      .
      .
    end

    describe "delete links" do

      it { should_not have_link('delete') }

      describe "as an admin user" do
        let(:admin) { FactoryGirl.create(:admin) }
        before do
          sign_in admin
          visit users_path
        end

        it { should have_link('delete', href: user_path(User.first)) }
        it "should be able to delete another user" do
          expect do
            click_link('delete', match: :first)
          end.to change(User, :count).by(-1)
        end
        it { should_not have_link('delete', href: user_path(admin)) }
      end
    end
  end
  .
  .
  .
end

Код приложения показывает удаляющие ссылки только если текущий пользователь является админом (Листинг 9.43). Обратите внимание на аргумент method: :delete, который организует выдачу ссылками необходимого запроса DELETE. Мы также обернули каждую ссылку в if выражение, таким образом они видны только администраторам. Результат, видимый нашим административным пользователям, представлен на Рис. 9.14.

Листинг 9.43. Ссылки удаляющие пользователей (видны только администраторам).
app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 52 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

Изначально, веб браузеры не могут отправлять DELETE запросы, и Rails подделывает их с помощью JavaScript. Это означает, что удаляющие ссылки не будут работать если у пользователя отключен JavaScript. Если вы обязаны поддерживать браузеры с отключенным JavaScript, вы можете подделать запрос DELETE с помощью формы и запроса POST, что будет работать даже без JavaScript; более подробно об этом см. RailsCast о “Destroy Without JavaScript”.

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

Для того чтобы получить рабочие удаляющие ссылки, нам необходимо добавить действие destroy (Таблица 7.1) которое будет находить соответствующего пользователя и удалять его с помощью метода Active Record destroy, по завершении перенаправляя пользователя на страницу списка пользователей, как это показано в Листинге 9.44. Обратите внимание, что мы также добавили :destroy в предфильтр signed_in_user.

Листинг 9.44. Добавление рабочего действия destroy.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted."
    redirect_to users_url
  end
  .
  .
  .
end

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

User.find(params[:id]).destroy

Даже несмотря на то, что только администраторы могут видеть ссылки на удаление, есть еще одна страшная дыра в безопасности: любой достаточно опытный злоумышленник может просто выдать запрос DELETE из командной строки и удалить любого пользователя на сайте. Для того, чтобы обеспечить безопасность сайта, мы также нуждаемся в контроле доступа, и наши тесты должны проверить не только то, что администраторы могут удалять пользователей, но также и то, что другие пользователи не могут этого делать. Результаты представлены в Листинге 9.45. Обратите внимание на то, что, по аналогии с методом patch из Листинга 9.11, мы используем delete для непосредственной выдачи DELETE к указанному URL (в данном случае, путь к пользователю, как того требует Таблица 7.1).

Листинг 9.45. Тест для защиты действия destroy.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do
    .
    .
    .
    describe "as non-admin user" do
      let(:user) { FactoryGirl.create(:user) }
      let(:non_admin) { FactoryGirl.create(:user) }

      before { sign_in non_admin, no_capybara: true }

      describe "submitting a DELETE request to the Users#destroy action" do
        before { delete user_path(user) }
        specify { expect(response).to redirect_to(root_url) }
      end
    end
  end
end

В принципе, у нас осталась еще одна незначительная брешь в безопасности, которая заключается в том, что админ может удалить сам себя выдав запрос DELETE. Можно, конечно, сказать что такой админ Сам Себе Злой Буратино, но было бы неплохо предотвратить подобные случаи, что остается в качестве упражнения (Раздел 9.6).

Как вы можете догадаться, реализация использует предфильтр, в этот раз для ограничения доступа к destroy действию всем пользователям кроме администраторов. Получившийся в результате предфильтр admin_user представлен в Листинге 9.46.

Листинг 9.46. Предфильтр открывающий доступ к действию destroy только админам.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

В этой точке все тесты должны проходить и ресурс Users — со своим контроллером, моделью и представлениями — функционально завершен.

$ bundle exec rspec spec/

9.5 Заключение

Мы прошли долгий путь с момента введения контроллера Users в Разделе 5.4. Те пользователи даже не могли зарегистрироваться; теперь же пользователи могут зарегистрироваться, войти в систему, выйти, просматривать свои профили, редактировать их параметры, и видеть список всех пользователей, а некоторые из них могут даже удалять других пользователей.

Остальная часть этой книги будет опираться на ресурс Users (и связанную с ним аутентификационную систему) чтобы сделать сайт с Twitter-подобными микросообщениями (Глава 10) и потоком сообщений пользователей за которыми следит данный пользователь (Глава 11). Эти главы представят некоторые из наиболее мощных возможностей Rails, включая моделирование данных с has_many и has_many through.

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

$ git add .
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users

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

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

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

$ heroku restart

Стоит также отметить, что в этой главе мы в последний раз имели необходимость в установке гема. Для справки, окончательный вариант Gemfile показан в Листинге 9.47. (Необязательные гемы которые могут оказаться системозависимыми закомментированы. Вы можете раскомментировать их для того чтобы посмотреть работают ли они на вашей системе.)

Листинг 9.47. Окончательный вариант Gemfile для примера приложения.
source 'https://rubygems.org'
ruby '2.0.0'
#ruby-gemset=railstutorial_rails_4_0

gem 'rails', '4.0.2'
gem 'bootstrap-sass', '2.3.2.0'
gem 'bcrypt-ruby', '3.1.2'
gem 'faker', '1.1.2'
gem 'will_paginate', '3.0.4'
gem 'bootstrap-will_paginate', '0.0.9'

group :development, :test do
  gem 'sqlite3', '1.3.8'
  gem 'rspec-rails', '2.13.1'
  # The following optional lines are part of the advanced setup.
  # gem 'guard-rspec', '2.5.0'
  # gem 'spork-rails', '4.0.0'
  # gem 'guard-spork', '1.5.0'
  # gem 'childprocess', '0.3.6'
end

group :test do
  gem 'selenium-webdriver', '2.35.1'
  gem 'capybara', '2.1.0'
  gem 'factory_girl_rails', '4.2.0'
  gem 'cucumber-rails', '1.4.0', :require => false
  gem 'database_cleaner', github: 'bmabey/database_cleaner'

  # Uncomment this line on OS X.
  # gem 'growl', '1.0.3'

  # Uncomment these lines on Linux.
  # gem 'libnotify', '0.8.0'

  # Uncomment these lines on Windows.
  # gem 'rb-notifu', '0.0.4'
  # gem 'win32console', '1.3.2'
  # gem 'wdm', '0.1.0'
end

gem 'sass-rails', '4.0.1'
gem 'uglifier', '2.1.1'
gem 'coffee-rails', '4.0.1'
gem 'jquery-rails', '3.0.4'
gem 'turbolinks', '1.1.1'
gem 'jbuilder', '1.0.2'

group :doc do
  gem 'sdoc', '0.3.20', require: false
end

group :production do
  gem 'pg', '0.15.1'
  gem 'rails_12factor', '0.0.2'
end

9.6 Упражнения

  1. Направив запрос PATCH напрямую к методу update как это показано в Листинге 9.48, проверьте что атрибут admin не может быть отредактирован через веб. Вначале получите провальный тест и лишь потом добейтесь его прохождения. (Подсказка: Вашим первым шагом должно стать добавление admin в список разрешенных параметров в user_params.)
  2. Организуйте открытие Gravatar ссылки “change” из Листинга 9.3 в новом окне (или вкладке). Подсказка: Ищите в сети; вы должны найти один простой и надежный метод с участием так называмого _blank.
  3. Текущий тест аутентификации проверяет что навигационные ссылки, такие как “Profile” и “Settings” появляются когда пользователь входит. Добавьте тест проверяющий что эти ссылки не видны невошедшим пользователям.
  4. Примените тестовый хелпер sign_in из Листинга 9.6 в как можно большем количестве мест.
  5. Удалите дублирующийся код формы отрефакторив new.html.erb и edit.html.erb представления с помощью партиала из Листинга 9.49. Обратите внимание на то, что вам придется явно передать пременную формы f в виде локальной переменной, как это показано в Листинге 9.50. Вам также понадобится обновить тесты, поскольку формы не останутся в точности такими же; найдите небольшую разницу между ними и обновите тесты соответствующим образом.
  6. Зарегистрированным пользователям совершенно незачем иметь доступ к new и create действиям контроллера Users. Организуйте для таких пользователей переадресацию в корневой URL, при попытке обратиться к этим страницам.
  7. Изучите объект request вставляя некоторые методы перечисленные в Rails API9 в шаблон сайта. (Обращайтесь к Листингу 7.1 если застрянете.)
  8. Напишите тест для проверки того, что дружелюбная переадресация направляет к данному URL только в первый раз. На последующие попытки входа, адрес перенаправления должени меняться на дефолтный (т.e., страницу профиля). См. подсказку в Листинге 9.51 (и подсказкой в данном случае я называю решение).
  9. Модифицируйте действие destroy так, чтобы предотвратить уничтожение административными пользователями самих себя. (Начните с написания теста.)
Листинг 9.48. Тестирование того что атрибут admin запрещен к редактированию через веб.
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "edit" do
    .
    .
    .
    describe "forbidden attributes" do
      let(:params) do
        { user: { admin: true, password: user.password,
                  password_confirmation: user.password } }
      end
      before do
        sign_in user, no_capybara: true
        patch user_path(user), params
      end
      specify { expect(user.reload).not_to be_admin }
    end
  end
end
Листинг 9.49. Партиал для полей форм new и edit.
app/views/users/_fields.html.erb
<%= render 'shared/error_messages' %>

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

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

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

<%= f.label :password_confirmation, "Confirm Password" %>
<%= f.password_field :password_confirmation %>
Листинг 9.50. Представление для создания нового пользователя с партиалом.
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(@user) do |f| %>
      <%= render 'fields', f: f %>
      <%= f.submit "Create my account", class: "btn btn-large btn-primary" %>
    <% end %>
  </div>
</div>
Листинг 9.51. Тест для перенаправления на дефолтную страницу после дружелюбной переадресации.
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      .
      .
      .
      describe "when attempting to visit a protected page" do
        before do
          visit edit_user_path(user)
          fill_in "Email",    with: user.email
          fill_in "Password", with: user.password
          click_button "Sign in"
        end

        describe "after signing in" do

          it "should render the desired protected page" do
            expect(page).to have_title('Edit user')
          end

          describe "when signing in again" do
            before do
              delete signout_path
              visit signin_path
              fill_in "Email",    with: user.email
              fill_in "Password", with: user.password
              click_button "Sign in"
            end

            it "should render the default (profile) page" do
              expect(page).to have_title(user.name)
            end
          end
        end
      end
    end
    .
    .
    .
  end
end
  1. Изображение взято на http://www.flickr.com/photos/sashawolff/4598355045/
  2. Сайт Gravatar на самом деле переадресует на http://en.gravatar.com/emails, предназначенную для англоговорящих пользователей, но я опустил en часть для возможного использования других языков. 
  3. Не беспокойтесь о том, как это работает, детали представляют интерес для разработчиков самого фреймворка Rails, но, по замыслу, не имеют значения для разработчиков Rails приложений. 
  4. Код в этом разделе это адаптация (переделка) гема Clearance команды разработчиков thoughtbot
  5. Спасибо пользователю Yoel Adler и за указание на этот нюанс и за поиск решения. 
  6. Фотография ребенка из http://www.flickr.com/photos/glasgows/338937124/
  7. Имя user непринципиально — мы могли бы написать @users.each do |foobar|, а затем использовать render foobar. Ключом является класс объекта — в данном случае, User
  8. Инструменты командной строки, такие как curl могут выдавать PATCH запросы такой формы. 
  9. http://api.rubyonrails.org/v4.0.0/classes/ActionDispatch/Request.html