Ruby on Rails Tutorial
Изучение Rails на Примерах
Майкл Хартл
Содержание
- Предисловие к русскому изданию
- Глава 1 От нуля к развертыванию
- Глава 2 demo app
- Глава 3 В основном статические страницы
- Глава 4 Rails-приправленный Ruby
- Глава 5 Заполнение шаблона
- Глава 6 Моделирование пользователей
- Chapter 7 Регистрация
- Глава 8 Войти, выйти
- Глава 9 Обновление, демонстрация и удаление пользователей
- Глава 10 Микросообщения пользователей
- Глава 11 Слежение за сообщениями пользователей
Предисловие
Моя компания (CD Baby) была одной из первых громко перешедших на Ruby on Rails, а затем еще громче вернувшейся обратно на PHP (Google расскажет вам об этой драме). Эту книгу, написанную Майклом Хартлом так высоко рекомендовали, что я должен был попробовать её, и Ruby on Rails Tutorial это всё, что я использовал, чтобы вернуться к Rails.
Хотя я уже прошел через много книг по Rails, это одна из немногих, что, наконец, зацепила меня. Было много написано книг типа «Путь Rails» — после которых я чувствовал себя неестественно, но после этой книги я наконец почувствовал себя естественно. Это также единственная книга по Rails, которая соблюдает методику «разработка через тестирование» на всем своем протяжении, этот подход строго рекомендуется специалистами, но он никогда не был так чётко продемонстрирован ранее. Наконец, Git, GitHub и Heroku присутствуют в демо-примерах, автор действительно дает вам почувствовать, что он хотел сделать реальный проект. Учебный код примеров не изолирован.
Линейное повествование — отличный формат. Лично я прошел Rails Tutorial в течении трёх долгих дней, делая все примеры и задачи в конце каждой главы. Делайте всё от начала до конца, не прыгая, и вы получите максимальную пользу.
Наслаждайтесь!
Derek Sivers (sivers.org)
Основатель CD Baby
Благодарности
Ruby On Rails Учебник во многом обязан моей предыдущей книге по Rails, RailsSpace и, следовательно, моему соавтору Aurelius Prochazka. Я хотел бы поблагодарить Aure как за работу, которую он проделал над прошлой книгой, так и за поддержку этой. Я также хотел бы поблагодарить Debra Williams Cauley, редактора обеих книг RailsSpace и Rails Tutorial; до тех пор, пока она не прекратит брать меня на бейсбол, я буду продолжать писать книги для нее.
Я хотел бы поблагодарить огромное количество Рубистов учивших и вдохновлявших меня на протяжении многих лет: David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Pratik Naik, Sarah Mei, Sarah Allen, Wolfram Arnold, Alex Chaffee, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, хороших людей из Pivotal Labs, команду Heroku, thoughtbot ребят, и команду GitHub. Наконец, многих, многих читателей - слишком много чтобы перечислять их здесь - внёсших большое количество предложений по улучшению и сообщивших об ошибках во время написания этой книги, и я с благодарностью признаю их помощь в написании ее настолько хорошей, насколько это было возможно.
Об авторе
Майкл Хартл – автор Ruby on Rails Tutorial, лидирующего введения в веб разработку на Ruby on Rails. Его предыдущий опыт включает в себя написание и разработку RailsSpace - чрезвычайно устаревшего учебника по Rails и разработку Insoshi - некогда популярной, а ныне устаревшей платформы для социальных сетей написанной на Ruby on Rails. В 2011, Майкл получил Ruby Hero Award за его вклад в Ruby сообщество. Он закончил Harvard College, имеет степень Кандидата Физических Наук присвоенную в Caltech и является выпускником предпринимательских курсов Y Combinator.
Копирайт и лицензия
Ruby on Rails Tutorial: Learn Web Development with Rails. Copyright © 2012 by Michael Hartl. Весь исходный код в Ruby on Rails Tutorial доступен под MIT License и Beerware License.
Лицензия MIT
Copyright (c) 2013 Michael Hartl
Данная лицензия разрешает лицам, получившим копию данного программного
обеспечения и сопутствующей документации (в дальнейшем именуемыми
«Программное Обеспечение»), безвозмездно использовать Программное
Обеспечение без ограничений, включая неограниченное право на использование,
копирование, изменение, добавление, публикацию, распространение,
сублицензирование и/или продажу копий Программного Обеспечения, также
как и лицам, которым предоставляется данное Программное Обеспечение,
при соблюдении следующих условий:
Указанное выше уведомление об авторском праве и данные условия должны быть
включены во все копии или значимые части данного Программного Обеспечения.
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО
ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ
ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ
ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ,
ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ
ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
/*
* ----------------------------------------------------------------------------
* "ПИВНАЯ ЛИЦЕНЗИЯ" (Ревизия 42):
* Весь код написан Майклом Хартлом. До тех пор пока вы осознаете это,
* вы можете делать с ним все что захотите. Если мы когда нибудь
* встретимся, и если это того стоило, вы можете купить мне
* пиво в ответ.
* ----------------------------------------------------------------------------
*/
Глава 7 Регистрация
Теперь, когда у нас есть рабочая модель User, пришло время добавить возможность, без которой некоторые сайты просто жить не могут: позволить пользователям регистрироваться на сайте. Мы будем использовать HTML форму для предоставления пользователями регистрационной информации нашему приложению в Разделе 7.2, которая затем будет использована для создания нового пользователя и сохранения его атрибутов в базе данных в Разделе 7.4. В конце процесса регистрации важно отобразить страницу профиля с информацией о новом созданном пользователе, так что мы начнем с создания страницы для демонстрации пользователей, которая будет для нас первым шагом на пути реализации REST архитектуры для пользователей (Раздел 2.2.2). Как обычно, мы будем писать тесты по мере разработки, расширяя наши познания о применении RSpec и Capybara для написания кратких и выразительных интеграционных тестов.
Для того чтобы создать страницу профиля пользователя, нам необходимо иметь пользователя в базе данных, что представлеяет проблему яйца и курицы: как сайт может иметь пользователя до появления рабочей страницы регистрации? К счастью, эта проблема уже решена: в Разделе 6.3.5 мы создавали запись User вручную с помощью Rails консоли. Если вы пропустили этот раздел, вам следует вернуться и закончить его перед продолжением.
Если вы используете управление версиями, создайте тему ветки, как обычно:
$ git checkout master
$ git checkout -b sign-up
7.1 Демонстрация пользователей
В этом разделе мы сделаем первый шаг на пути к конечной странице пользователя, создав страницу для отображения имени и фотографии пользователя, на что указывает набросок страницы на Рис. 7.1.1 Нашей конечной целью для страниц профиля пользователя является отображение фотографии пользователя, основных данных пользователя и списка микросообщений, как показано на наброске в Рис. 7.2.2 (на Рис. 7.2 показан наш первый пример lorem ipsum текста, который имеет захватывающую историю которую вам решительно стоит прочитать на досуге.) Мы закончим эту задачу, а вместе с ней и пример приложения, в Главе 11.
7.1.1 Отладка и окружения Rails
Профили в этом разделе будут первыми по-настоящему динамическими страницами в нашем приложении. Хотя представление будет существовать как одна страница кода, каждый профиль будет кастомизирован с использованием информации получаемой из базы данных сайта. В качестве подготовки к добавлению динамических страниц в наш пример приложения, сейчас хорошее время, чтобы добавить некоторую отладочную информацию к шаблону нашего сайта (Листинг 7.1). Это отображает некоторую полезную информацию о каждой странице с помощью встроенного debug
метода и params
переменной (о которой мы узнаем больше в Разделе 7.1.2).
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
.
.
.
<body>
<%= render 'layouts/header' %>
<div class="container">
<%= yield %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
</body>
</html>
Для того чтобы вывод отладочной информации приятно выглядел, мы добавим несколько правил к кастомной таблице стилей созданной в Главе 5, как это показано в Листинге 7.2.
app/assets/stylesheets/custom.css.scss
@import "bootstrap";
/* mixins, variables, etc. */
$grayMediumLight: #eaeaea;
@mixin box_sizing {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.
.
.
/* miscellaneous */
.debug_dump {
clear: both;
float: left;
width: 100%;
margin-top: 45px;
@include box_sizing;
}
Этот код вводит новую для нас возможность Sass - примесь, в данном случае названную box_sizing
. Примесь позволяет группировать CSS правила с тем чтобы они могли использоваться для нескольких элементов, конвертируя
.debug_dump {
.
.
.
@include box_sizing;
}
в
.debug_dump {
.
.
.
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
Мы еще раз применим эту примесь в Разделе 7.2.2. Результат ее применения в контексте блока с отладочной информацией показан на Рис. 7.3.
Вывод отладочной информации на Рис. 7.3 дает потенциально полезную информацию об отображаемой в настоящий момент странице:
---
controller: static_pages
action: home
Это YAML3 представление params
, который по сути является хэшем и в данном случае указывает на контроллер и действие страницы. Мы увидим еще один пример в Разделе 7.1.2
Поскольку мы не хотим показывать отладочную информацию пользователям развернутого приложения, Листинг 7.1 использует
if Rails.env.development?
для того чтобы показывать отладочную информацию только в среде разработки, которая является одной из трех сред, определенных по умолчанию в Rails (Блок 7.1).4 В частности, Rails.env.development?
является true
только в среде разработки, так что Embedded Ruby
<%= debug(params) if Rails.env.development? %>
не будет вставлен в развернутое приложение или тесты. (Вставка отладочной информации в тесты возможно не навредит, но скорее всего и не сделает ничего хорошего, так что лучше ограничиться ее отображением только в среде разработки.)
Rails поставляется с тремя окружениями: test, development и production. Дефолтным окружением для Rails консоли является development:
$ rails console Loading development environment >> Rails.env => "development" >> Rails.env.development? => true >> Rails.env.test? => false
Как вы можете видеть, Rails обеспечивает объект Rails атрибутом env и связанными с ним булевыми методами окружения, таким образом, например, Rails.env.test? возвращает true в тестовом окружении и false во всех остальных.
Если вам когда либо понадобится запустить консоль в окружении, отличном от дефолтного (для отладки теста, например), вы можете предать окружение в качестве параметра к console скрипту:
$ rails console test Loading test environment >> Rails.env => "test" >> Rails.env.test? => true
Как и в случае с консолью, development является дефолтным окружением для локального Rails сервера, но вы также можете запустить его и в другом окружении:
$ rails server --environment production
Если вы попробуете посмотреть на свое приложение запущенное в production, оно не будет работать без production базы данных, которую мы можем создать запустив rake db:migrate в production:
$ bundle exec rake db:migrate RAILS_ENV=production
(Я считаю запутывающим, когда консоль, сервер и команды миграций устанавливают не дефолтные окружения тремя взаимно исключающими способами, поэтому я дал себе труд показать все три.)
Кстати, если вы развернули свое приложение на Heroku, вы можете посмотреть его окружение с помощью команды heroku, которая предоставляет свою собственную (удаленную) консоль:
$ heroku run console Ruby console for yourapp.herokuapp.com >> Rails.env => "production" >> Rails.env.production? => true
Вполне естественно, что, поскольку Heroku является платформой для production сайтов, он запускает каждое приложение в production окружении.
7.1.2 Ресурс Users
В конце Главы 6 мы создали нового пользователя в базе данных. Как показано в Разделе 6.3.5, этот пользователь имеет id 1
и наша цель теперь заключается в создании страницы для отображения информации этого пользователя. Мы будем следовать соглашениям REST архитектуры предпочитаемой в Rails приложениях (Блок 2.2), что означает представление данных в качестве ресурсов которые могут быть созданы, показаны, обновлены, или уничтожены — четыре действия, соответствующие четырем фундаментальным операциям POST, GET, PATCH и DELETE определенным стандартом HTTP (Блок 3.3).
Следуя принципам REST, на ресурсы обычно ссылаются, используя имя ресурса и уникальный идентификатор. Что это означает в контексте пользователей — о которых мы теперь думаем, как о ресурсе Users—то, что мы должны показать пользователя с id 1
выдав GET запрос к URL /users/1. Здесь show
действие неявно в типе запроса —когда Rails’ функции REST активированы, GET запросы автоматически обрабатываются show
действием.
Мы видели в Разделе 2.2.1 что страница для пользователя с id 1
имеет URL /users/1. К сожалению, сейчас посещение этой страницы лишь выдает ошибку (Рис. 7.4).
Мы можем заполучить REST-style Users URL на работу, добавив одну-единственную строку в наш файл маршрутов (config/routes.rb
):
resources :users
Результат представлен в Листинге 7.3.
config/routes.rb
SampleApp::Application.routes.draw do
resources :users
root 'static_pages#home'
match '/signup', to: 'users#new', via: 'get'
.
.
.
end
Вы возможно заметили, что листинг 7.3 удаляет строку
get "users/new"
последний раз замеченную в Листинге 5.35. Это связано с тем, что resources :users
не просто добавляет работающий /users/1 URL; эта строка также обеспечивает наш пример приложения всеми действиями, необходимыми для RESTful (полностью REST) ресурса Users,5 наряду с большим количеством именованных маршрутов (Раздел 5.3.3) для генерации URL пользователя. Получившееся соответствие URL, действий и именованных маршрутов показано в Таблице 7.1. (Сравните с Таблицей 2.2.) В течение следующих трех глав, мы охватим остальные записи в Таблице 7.1 поскольку мы заполним все действия, необходимые, для того, чтобы сделать Users RESTful ресурсом.
HTTP запрос | URL | Действие | Именованный маршрут | Назначение |
---|---|---|---|---|
GET | /users | index | users_path | страница показывающая список всех пользователей |
GET | /users/1 | show | user_path(user) | страница показывающая пользователя |
GET | /users/new | new | new_user_path | страница для создания нового пользователя (регистрация) |
POST | /users | create | users_path | создание нового пользователя |
GET | /users/1/edit | edit | edit_user_path(user) | страница для редактирования пользователя с id 1 |
PATCH | /users/1 | update | user_path(user) | обновление пользователя |
DELETE | /users/1 | destroy | user_path(user) | удаление пользователя |
С кодом в Листинге 7.3, маршруты работают, но страница по-прежнему не существует (Рис. 7.5). Для того чтобы исправить это, мы начнем с минималистичной версии страницы профиля, которую мы откормим в Разделе 7.1.4.
Мы будем использовать стандартное Rails размещение для представления показывающего пользователя, которым является app/views/users/show.html.erb
. В отличие от представления new.html.erb
, которое мы создали с помощью генератора в Листинге 5.31, файл show.html.erb
в данный момент не существует, так что нам необходимо создать его вручную и заполнить его содержимым Листинга 7.4.
app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>
Это представление использует Embedded Ruby для отображения имени и адреса электронной почты пользователя, предполагая наличие переменной экземпляра @user
. Конечно же, в итоге реальная страница пользователя будет выглядеть совершенно иначе и не будет публично демонстрировать адрес электронной почты.
Для того чтобы заставить представление user show работать, нам необходимо определить переменную @user
в соответствующем show
действии контроллера Users. Как вы и ожидали, мы используем метод find
на модели User (Раздел 6.1.4) для получения пользователя из базы данных, как это показано в Листинге 7.5.
show
action. app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
end
end
Здесь мы использовали params
для получения id пользователя. Когда мы сделаем соответствующий запрос в контроллер Users, params[:id]
будет пользовательским id 1, так что эффект тот же что и с методом find
User.find(1)
который мы видели в Разделе 6.1.4. (Технически, params[:id]
является строкой "1"
, но find
достаточно умен, чтобы преобразовать это в целое число.)
С представлением и определенным действием, URL /users/1 работает замечательно (Рис. 7.6). Обратите внимание что отладочная информация в Рис. 7.6 подтверждает значение params[:id]
:
---
action: show
controller: users
id: '1'
Вот почему код
User.find(params[:id])
в Листинге 7.5 находит пользователя с id 1.
7.1.3 Тестирование страницы показывающей пользователя (с фабриками)
Теперь, когда у нас есть минимально рабочий профиль, пришло время поработать над версией из наброска на Рис. 7.1. Как и в случае с созданием статических страниц (Глава 3) и моделью User (Глава 6), мы сделаем это используя разработку через тестирование.
Вспомните из Раздела 5.4.2 что для страниц связанных с ресурсом Users мы приняли решение использовать интеграционные тесты. В случае со страницей регистрации, наш тест вначале посещает signup_path
а затем проверяет правильность h1
и title
тегов, как это показано в Листинге 5.34 и повторено в Листинге 7.6. (Обратите внимание, что мы опустили full_title
хелпер из Раздела 5.3.4 поскольку полный заголовок уже адекватно протестирован.)
spec/requests/user_pages_spec.rb
require 'spec_helper'
describe "User pages" do
subject { page }
describe "signup page" do
before { visit signup_path }
it { should have_content('Sign up') }
it { should have_title(full_title('Sign up')) }
end
end
Для того чтобы протестировать страницу показывающую пользователя, нам необходим объект модели User для того чтобы коду в действии show
(Листинг 7.5) было что искать:
describe "profile page" do
# Replace with code to make a user variable
before { visit user_path(user) }
it { should have_content(user.name) }
it { should have_title(user.name) }
end
где нам нужно заполнить коментарий соответствующим кодом. Здесь используется именованный маршрут user_path
(Таблица 7.1) для генерации пути к странице показывающей данного пользователя. Затем мы тестируем что и страница и тайтл содержат имя пользователя.
Для того чтобы создать необходимый объект модели User, мы можем использовать Active Record для создания пользователя посредством User.create
, но практика показывает, что пользовательские фабрики являются более удобным способом определения и вставки в базу данных объектов user. Мы будем использовать фабрики генерируемые Factory Girl - Ruby гемом который сделан хорошими людьми из thoughtbot. Как и RSpec, Factory Girl определяет предметно-ориентированный язык в Ruby, в данном случае предназначенный для определения объектов Active Record. Синтаксис прост, опирается на Ruby блоки и кастомные методы для определения атрибутов описываемого объекта. Для случаев вроде тех что мы увидим в этой главе, преимущество над Active Record может быть неочевидным, но мы будем использовать более продвинутые техники фабрик в последующих главах. Например, в Разделе 9.3.3 нам понадобится создать набор пользователей с уникальными адресами электронной почты и фабрики позволят нам проделать это с легкостью.
Как и с другими Ruby гемами, мы можем установить Factory Girl добавив строку в Gemfile
используемый Bundler-ом (Listing 7.7). (Поскольку Factory Girl нужна только в тестах, мы поместили ее в группу :test
.)
Gemfile
.source 'https://rubygems.org'
.
.
.
group :test do
.
.
.
gem 'factory_girl_rails', '4.2.1'
end
.
.
.
Затем устанавливаем как обычно:
$ bundle install
Мы поместим все наши фабрики Factory Girl в файл spec/factories.rb
, который автоматически будет загружен RSpec-ом. Код необходимый для создания фабрики User представлен в Листинге 7.8.
spec/factories.rb
FactoryGirl.define do
factory :user do
name "Michael Hartl"
email "[email protected]"
password "foobar"
password_confirmation "foobar"
end
end
Передавая символ :user
команде factory
, мы говорим Factory Girl что последующее определение предназначено для объекта модели User.
С определением в Листинге 7.8, мы можем создать фабрику User в тестах используя команду let
(Блок 6.3) и метода FactoryGirl
поддерживаемого Factory Girl:
let(:user) { FactoryGirl.create(:user) }
Конечный результат представлен в Листинге 7.9.
spec/requests/user_pages_spec.rb
require 'spec_helper'
describe "User pages" do
subject { page }
describe "profile page" do
let(:user) { FactoryGirl.create(:user) }
before { visit user_path(user) }
it { should have_content(user.name) }
it { should have_title(user.name) }
end
describe "signup page" do
before { visit signup_path }
it { should have_content('Sign up') }
it { should have_title(full_title('Sign up')) }
end
end
В этой точке вам необходимо проверить что набор тестов в красном:
$ bundle exec rspec spec/
Мы можем получить зеленые тесты с кодом из Листинга 7.10.
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<h1><%= @user.name %></h1>
Повторный запуск тестов должен подтвердить что тесты из Листинга 7.9 теперь проходят:
$ bundle exec rspec spec/
Вы быстро заметите что тесты с Factory Girl - медленные. И это происходит не по вине Factory Girl и фактически это фича, а не баг. Проблема связана с тем, что алгоритм BCrypt используемый в Разделе 6.3.1 для создания хэша безопасного пароля является медленным по своей природе: именно низкая скорость BCrypt отчасти делает его таким сложным для взлома. К сожалению, это означает что создание пользователей может утопить набор тестов; к счастью, есть простой способ исправить это. Библиотека bcrypt-ruby использует фактор стоимости для управления вычислительной сложностью создаваемого безопасного хэша. Дефолтное значение призвано обеспечить безопасность, а не скорость и для рабочих приложений это здорово, но в тестах наши потребности противоположны: мы хотим быстрые тесты и нас совершенно не тревожит безопасность хэшей паролей тестовых пользователей. Решением будет добавление строки которая переопределит фактор стоимости от дефолтного безопасного значения к минимально возможному в файл конфигурации тестового окружения config/environments/test.rb
, как это показано в Листинге 7.11. Даже для небольшого набора тестов выигрыш в скорости от этого шага может быть весьма значительным и я настоятельно рекомендую включить Листинг 7.11 в ваш test.rb
конфиг.
config/environments/test.rb
SampleApp::Application.configure do
.
.
.
# Speed up tests by lowering bcrypt's cost function.
ActiveModel::SecurePassword.min_cost = true
end
7.1.4 Изображение Gravatar и боковая панель
Определив базовую страницу пользователя в предыдущем разделе, теперь мы немного улучшим ее, добавив изображение пользователя и начальную реализацию боковой панели пользователя. При создании страницы мы сфокусируемся на ее внешнем виде, не особо заботясь о ее структуре, это означает, что (по крайней мере пока) мы не будем писать тесты. Когда мы подойдем к аспектам представления, более подверженным ошибкам, таким как пагинация (Раздел 9.3.3), мы продолжим разработку через тестирование.
Мы начнем с добавления “глобально распознаваемого аватара” (также известного как Граватар) к профилю пользователя.6 Автором Граватар является Tom Preston-Werner (сооснователь GitHub), впоследствии его (Граватар) приобрела компания Automattic (создатели WordPress), это бесплатный сервис который позволяет пользователям загружать изображения и связывать их с подконтрольными им адресами электронной почты. Gravatar это удобный способ добавить изображения пользователей не связываясь с проблемами загрузки изображений, их обрезкой и хранением; все что нам нужно, это создать правильный URL Gravatar изображения используя адрес электронной почты пользователя и соответствующее изображение Gravatar появится автоматически.7
Мы планируем определить вспомогательную функцию gravatar_for
которая будет возвращать изображение Граватар для данного пользователя, как это показано в Листинге 7.12.
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
В этой точке вы можете проверить что набор тестов не проходит:
$ bundle exec rspec spec/
Из-за того что метод gravatar_for
неопределен, страница показывающая пользователя в настоящий момент не работает. (Отлов подобных ошибок это, возможно самый полезный аспект тестов представлений. Вот почему так важно наличие хоть каких-то тестов представлений, даже минималистичных.)
По умолчанию, методы определеннные в любом файле хелпера является автоматически доступным в любом представлении, но для удобства мы поместим метод gravatar_for
в файл для хелперов, связанных с контроллером Users. Как отмечено на главной странице Граватар, URL Граватара основывается на MD5 хэше адреса электронной почты пользователя. В Ruby, алгоритм MD5 хэширования реализутся с помощью метода hexdigest
, который является частью библиотеки Digest
:
>> email = "[email protected]".
>> Digest::MD5::hexdigest(email.downcase)
=> "1fda4469bcbec3badf5418269ffc5968"
Поскольку адреса электронной почты нечувствительны к регистру (Раздел 6.2.4), в отличие от MD5 хэшей, мы используем метод downcase
для того чтобы быть уверенными в том, что аргумент передаваемый в hexdigest
находится в нижнем регистре. Получившийся хелпер gravatar_for
представлен в Листинге 7.13.
gravatar_for
. app/helpers/users_helper.rb
module UsersHelper
# Returns the Gravatar (http://gravatar.com/) for the given user.
def gravatar_for(user)
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
Код в Листинге 7.13 возвращает тег img
для Граватара с классом "gravatar"
и альтернативным текстом эквивалентным имени пользователя (что особенно удобно для браузеров используемых слабовидящими людьми которые работают с помощью считывателей экрана). Вы можете проверить что теперь набор тестов проходит:
$ bundle exec rspec spec/
Страница профиля представленная на Рис. 7.7, показывающая дефолтное изображение Граватара, выглядит подобным образом т.к. [email protected]
это невалидный email адрес (домен example.com зарезервирован для примеров (examples).
Для того чтобы отобразить кастомный Граватар в нашем приложении, мы будем использовать update_attributes
(Раздел 6.1.5) для обновления пользователя в базе данных:
$ rails console
>> user = User.first
>> user.update_attributes(name: "Example User",
?> email: "[email protected]",
?> password: "foobar",
?> password_confirmation: "foobar")
=> true
Здесь мы назначили пользователю адрес электронной почты [email protected]
, который я связал с логотипом Rails Tutorial, как это видно на Рис. 7.8.
Последний элемент, необходимый для завершения наброска из Рис. 7.1 это начальная версия пользовательской боковой панели. Мы реализуем ее с помощью тега aside
, который используется для элементов (таких как сайдбары), которые дополняют страницу, но также могут быть использованы отдельно. Мы включаем row
и span4
классы, которые являются частью Bootstrap. Код для измененной страницы показывающей пользователя представлен в Листинге 7.14.
show
. app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
<aside class="span4">
<section>
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
</aside>
</div>
Имея соответствующие HTML элементы и CSS классы, мы можем отстилить страницу профиля (включая боковую панель и Gravatar) с SCSS показанным в Листинге 7.15. (Обратите внимание на наследование CSS правил, которое работает только благодаря препроцессору Sass, используемому файлопроводом.) Получившаяся страница показана на Рис. 7.9.
app/assets/stylesheets/custom.css.scss
.
.
.
/* sidebar */
aside {
section {
padding: 10px 0;
border-top: 1px solid $grayLighter;
&:first-child {
border: 0;
padding-top: 0;
}
span {
display: block;
margin-bottom: 3px;
line-height: 1;
}
h1 {
font-size: 1.4em;
text-align: left;
letter-spacing: -1px;
margin-bottom: 3px;
margin-top: 0px;
}
}
}
.gravatar {
float: left;
margin-right: 10px;
}
7.2 Форма регистрации
Теперь, когда у нас есть рабочая (хоть и незавершенная) страница профиля пользователя, мы готовы приступить к созданию формы для регистрации на нашем сайте. Мы видели на Рис. 5.9 (дублированном на Рис. 7.10) что страница регистрации в настоящий момент пуста и совершенно бесполезна для регистрации новых пользователей. Целью этого раздела является разработка формы регистрации из Рис. 7.11.
Поскольку мы говорим о создании пользователей через веб-интерфейс, давайте удалим пользователя созданного через консоль в Разделе 6.3.5. Простейшим способом сделать это является очистка базы данных с помощью Rake-задачи db:reset
:
$ bundle exec rake db:reset
После очистки базы данных на некоторых системах также необходимо заново подготовить тестовую базу данных:
$ bundle exec rake test:prepare
Наконец, на некоторых системах вам может потребоваться перезагрузка сервера для того чтобы изменения вступили в силу.8
7.2.1 Тесты для регистрации пользователя
До появления мощных веб-фреймворков с полным набором тестировочных средств, тестирование часто доставляло много хлопот. Например, для тестирования страницы регистрации вручную, нам бы пришлось посещать страницу в браузере, затем заполнять форму различными валидными и невалидными данными и проверять что в каждом случае поведение приложения соответствует ожидаемому. Кроме того, нам бы пришлось проделывать это каждый раз при изменении приложения. С помощью RSpec и Capybara мы сможем написать выразительные тесты, которые автоматизируют задачи, которые раньше делались вручную.
Мы уже видели как Capybara поддерживает интуитивно понятный синтаксис веб-навигации. До сих пор мы в основном использовали visit
для посещения конкретных страниц, но Capybara может гораздо больше, включая заполнение полей, вроде тех что мы видели на Рис. 7.11 и кликанья по кнопке. Синтаксис выглядит примерно так:
visit signup_path
fill_in "Name", with: "Example User"
.
.
.
click_button "Create my account"
Сейчас нашей целью является написание тестов для правильного поведения при предоставлении невалидной и валидной регистрационной информации. Поскольку эти тесты довольно продвинутые, мы будем писать их постепенно. Если вам интересно посмотреть как они работают (включая файл в который они должны быть помещены), вы можете перейти непосредственно к Листингу 7.16.
Наша первая задача это тестирование провальной отправки регистрационной формы, и мы можем симулировать отправку невалидных данных посетив страницу и кликнув по кнопке с помощью click_button
:
visit signup_path
click_button "Create my account"
Это эквивалентно посещению страницы регистрации и отправке формы незаполненной регистрационной информацией. Аналогично, для симуляции отправки валидных данных, мы заполняем форму валидными данными с помощью fill_in
:
visit signup_path
fill_in "Name", with: "Example User"
fill_in "Email", with: "[email protected]"
fill_in "Password", with: "foobar"
fill_in "Confirmation", with: "foobar"
click_button "Create my account"
Эти тесты проверяют что поведение приложения после клика по кнопке “Create my account” соответствует нашим ожиданиям: создается новый пользователь если информация валидна и не создается новый пользователь в случае когда она невалидна. Для того чтобы сделать это мы проверяем количество пользователей и наши тесты будут использовать метод count
, доступный для каждого класса Active Record, включая User
:
$ rails console
>> User.count
=> 0
Здесь User.count
является 0
поскольку мы очистили базу данных в начале этого раздела.
При отправке невалидных данных, мы ожидаем что количество пользователей не будет изменено; при отправке валидных данных мы ожидаем что оно изменится на 1. Мы можем выразить это в RSpec скомбинировав метод expect
с методом to
или методом not_to
. Мы начнем со случая с невалидными данными поскольку он проще; мы посетим страницу регистрации и кликнем по кнопке, и мы ожидаем что это не изменит количества пользователей:
visit signup_path
expect { click_button "Create my account" }.not_to change(User, :count)
Обратите внимание: expect
обворачивает click_button
в блок (Раздел 4.3.2). Что необходимо для работы метода change
, который принимает в качестве аргумента объект и символ, а затем вычисляет результат вызова этого символа в качестве метода на объекте до и после блока. Другими словами, код
expect { click_button "Create my account" }.not_to change(User, :count)
вычисляет
User.count
до и после выполнения
click_button "Create my account"
В данном случае, мы хотим чтобы данный код не изменял количества, что мы можем выразить с помощью метода not_to
. В результате, закрыв клик по кнопке в блоке, мы можем заменить
initial = User.count
click_button "Create my account"
final = User.count
expect(initial).to eq final
на одну строку
expect { click_button "Create my account" }.not_to change(User, :count)
которая читается как естественный язык и является намного более компактной (eq
это RSpec метод для тестирования на равенство).
Случай с валидными данными аналогичен, но вместо проверки того, что количество пользователей не изменилось, мы проверяем что клик по кнопке изменяет их количество на 1:
visit signup_path
fill_in "Name", with: "Example User"
fill_in "Email", with: "[email protected]"
fill_in "Password", with: "foobar"
fill_in "Confirmation", with: "foobar"
expect do
click_button "Create my account"
end.to change(User, :count).by(1)
Здесь используется метод to
поскольку мы ожидаем что клик по кнопке (вкупе с валидными данными) изменит количество пользователей на единицу.
Комбинирование двух случаев с соответствующими блоками describe
и выталкивание общего кода в блоки before
, приводит нас к хорошим базовым тестам для регистрации пользователей, как это показано в Листинге 7.16. Здесь мы вынесли общий текст для кнопки регистрации в переменную submit
, которую мы определили с помощью метода let
.
spec/requests/user_pages_spec.rb
require 'spec_helper'
describe "User pages" do
subject { page }
.
.
.
describe "signup page" do
before { visit signup_path }
let(:submit) { "Create my account" }
describe "with invalid information" do
it "should not create a user" do
expect { click_button submit }.not_to change(User, :count)
end
end
describe "with valid information" do
before do
fill_in "Name", with: "Example User"
fill_in "Email", with: "[email protected]"
fill_in "Password", with: "foobar"
fill_in "Confirmation", with: "foobar"
end
it "should create a user" do
expect { click_button submit }.to change(User, :count).by(1)
end
end
end
end
Мы будем добавлять тесты по мере надобности в последующих разделах, но базовые тесты в Листинге 7.16 уже покрывают внушительный объем функционала. Для того чтобы получить их прохождение, мы должны создать страницу регистрации с необходимым минимумом правильных элементов, предопределить направление пользователя в правильное место после отправки формы и создание нового пользователя в базе данных только в случае если полученная информация валидна.
Конечно же, в этой точке тесты должны быть провальными:
$ bundle exec rspec spec/
7.2.2 Применение form_for
Теперь, когда у нас есть хорошие провальные тесты для регистрации пользователя, мы начнем добиваться их прохождения, для начала создав форму для регистрации пользователей. Мы можем сделать это в Rails с помощью вспомогательного метода form_for
который принимает объект Active Record и конструирует форму используя атрибуты объекта. Результат представлен в Листинге 7.17. (Читатели знакомые с Rails 2.x должны обратить внимание что form_for
использует “процент-равно” ERb синтаксис для вставки контента; там где Rails 2.x использовали <% form_for ... %>, мы теперь используем <%= form_for ... %>.)
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(@user) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation %>
<%= f.submit "Create my account", class: "btn btn-large btn-primary" %>
<% end %>
</div>
</div>
Давайте рассмотрим этот код по частям. Наличие ключевого слова do
указывает на то что form_for
принимает блок с одной переменной, которую мы назвали f
(от “form”):
<%= form_for(@user) do |f| %>
.
.
.
<% end %>
Как это зачастую происходит с хелперами Rails, нам нет надобности знать о подробностях реализации, но нам необходимо знать что делает объект f
: при вызове с методом соответствующим элементу HTML формы — таким как текстовое поле, радио кнопка, или поле пароля — он возвращает особым образом организованный код для этого элемента для установки атрибута объекта @user
. Другими словами,
<%= f.label :name %>
<%= f.text_field :name %>
создает HTML необходимый для создания элемента маркированного текстового поля для назначения атрибута name
модели User. (Мы взглянем на сам HTML в Разделе 7.2.3.)
Для того чтобы увидеть это в действии нам необходимо посмотреть на HTML производимый этой формой, но здесь мы сталкиваемся с проблемой: страница в настоящее время сломана, поскольку мы не установили переменную @user
— как все неопределенные переменные экземпляра (Раздел 4.4.5), @user
в настоящее время является nil
. Соответственно, если вы запустите ваш набор тестов в этой точке, вы увидите что тесты для содержимого страницы регистрации из Листинга 7.6 сейчас не проходят:
$ bundle exec rspec spec/requests/user_pages_spec.rb -e "signup page"
(Флаг -e
в данном случае позволяет запустить только спеки, чье описание совпадает с "signup page"
. В частности, обратите внимание что это не подстрока "signup"
, что привело бы к запуску всех тестов в Листинге 7.16.) Для того чтобы вновь сделать эти тесты проходящими и для того чтобы отрендерить нашу форму, мы должны определить переменную @user
в действии контроллера, соответствующем new.html.erb
, т.е., в действии new
контроллера Users. Хелпер form_for
ожидает что @user
будет объектом User и поскольку мы создаем нового пользователя, мы просто используем User.new
, как это видно в Листинге 7.18.
@user
в действие new
. app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def new
@user = User.new
end
end
После определения переменной @user
, тесты для страницы регистрации должны пройти:
$ bundle exec rspec spec/requests/user_pages_spec.rb -e "signup page"
В этой точке у нас должна получиться форма (со стилями из Листинга 7.19) выглядящая как на Рис. 7.12. Обратите внимание на повторное использование примеси box_sizing
из Листинга 7.2.
app/assets/stylesheets/custom.css.scss
.
.
.
/* forms */
input, textarea, select, .uneditable-input {
border: 1px solid #bbb;
width: 100%;
margin-bottom: 15px;
@include box_sizing;
}
input {
height: auto !important;
}
7.2.3 HTML формы
Как нам подсказывает Рис. 7.12, теперь страница регистрации рендерится как следует, что указывает на то, что код form_for
в Листинге 7.17 производит валидный HTML. Если вы взглянете на HTML для сгенерированной формы (используя Firebug или функцию “просмотр исходного кода страницы” вашего браузера), вы должны увидеть разметку как в Листинге 7.20. Хотя большая часть деталей несущественна для наших целей, давайте улучим минутку и ознакомимся с наиболее важными частями ее структуры.
<form accept-charset="UTF-8" action="/users" class="new_user"
id="new_user" method="post">
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />
<label for="user_email">Email</label>
<input id="user_email" name="user[email]" type="text" />
<label for="user_password">Password</label>
<input id="user_password" name="user[password]"
type="password" />
<label for="user_password_confirmation">Confirmation</label>
<input id="user_password_confirmation"
name="user[password_confirmation]" type="password" />
<input class="btn btn-large btn-primary" name="commit" type="submit"
value="Create my account" />
</form>
(Здесь я опустил HTML связанный с authenticity token, который Rails автоматически включают для предотвращения определенного вида атак, назывемого подделка межсайтовых запросов (CSRF). См введение в Rails authenticity token на Stack Overflow если вас интересуют подробности того как это работает и почему это так важно.)
Мы начнем с внутренней структуры документа. Сравнивая Листинг 7.17 с Листингом 7.20, мы видим что Embedded Ruby
<%= f.label :name %>
<%= f.text_field :name %>
производит HTML
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />
и
<%= f.label :password %>
<%= f.password_field :password %>
производит HTML
<label for="user_password">Password</label><br />
<input id="user_password" name="user[password]" type="password" />
Как видно из Рис. 7.13, текстовые поля (type="text"
) просто отображают их содержимое, в то время как поля паролей (type="password"
) скрывают вводимое в целях безопасности, как это показано на Рис. 7.13.
Как мы увидим в Разделе 7.4, ключом к созданию пользователя является специальный name
атрибут в каждом input
:
<input id="user_name" name="user[name]" - - - />
.
.
.
<input id="user_password" name="user[password]" - - - />
Эти значения name
позволяют Rails сконструировать хэш инициализации (через переменную params
) для создания пользователей с использованием значений введеных пользователем, как мы это увидим в Разделе 7.3.
Второй важный элемент это сам тег form
. Rails создает тег form
используя объект @user
: поскольку каждый объект в Ruby знает свой класс (Раздел 4.4.1), Rails определяет что @user
принадлежит к классу User
; кроме того, поскольку @user
это новый пользователь, Rails знает что необходимо построить форму с post
методом, который является правильным глаголом для создания нового объекта (Блок 3.3):
<form action="/users" class="new_user" id="new_user" method="post">
Здесь атрибуты class
и id
, по большому счету, не имеют особого значения; более важными являются action="/users"
и method="post"
. Совместно они формируют инструкцию для отправки HTTP запроса POST на URL /users. В следующих двух разделах мы увидим к чему это приводит.
7.3 Провальная регистрация
Мы кратко рассмотрели HTML формы показанной на Рис. 7.12 (Листинг 7.20), теперь мы копнем эту тему чуть глубже. Так как HTML формы лучше всего понимается в контексте сбоя регистрации, в этом разделе мы создадим регистрационную форму которая принимает невалидные данные и вновь рендерит страницу регистрации со списком ошибок, как это показано на наброске Рис. 7.14.
7.3.1 Рабочая форма
Вспомните из Раздела 7.1.2 что добавление resources :users
в файл routes.rb
(Листинг 7.3) автоматически обеспечивает наше Rails приложение возможностью отвечать на RESTful URL из Таблицы 7.1. В частности, это приводит к тому, что POST запрос к /users обрабатывается действием create
. Наша стратегия для действия create
зaключается в использовании отправки формы для создания объекта нового пользователя с помощью User.new
, попытке (провальной) сохранить этого пользователя и последующем рендеринге страницы регистрации для возможной повторной отправки формы. Давайте начнем с того что еще раз взглянем на код для формы регистрации:
<form action="/users" class="new_user" id="new_user" method="post">
Как было отмечено в Разделе 7.2.3, этот HTML выдает POST запрос к /users URL.
Нашим первым шагом на пути к зеленым тестам для невалидных данных из Листинга 7.16 будет добавление кода из Листинга 7.21. Этот листинг включает второе использование метода render
который мы впервые видели в контексте партиалов (Раздел 5.1.3); как вы можете видеть, render
также работает и в действиях контроллера. Обратите внимание что мы воспользовались этой возможностью чтобы представить ветвящуюся структуру if
-else
, которая позволяет нам обрабатывать случаи сбоя и успеха раздельно, в зависимости от значения @user.save
, которое (как мы видели в Разделе 6.1.3) может быть либо true
либо false
в зависимости от успешности сохранения.
create
которое может обрабатывать провальную регистрацию (но не успешную). app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(params[:user]) # Not the final implementation!
if @user.save
# Handle a successful save.
else
render 'new'
end
end
end
Обратите внимание на комментарий: это не конечная реализация, но этого достаточно для начала. Мы закончим реализацию в Разделе 7.3.2.
Лучший способ понять как работает код в Листинге 7.21 это отправить форму с какими-нибудь невалидными регистрационными данными; результат представлен на Рис. 7.15 и полная отладочная информация (с увеличенным размером шрифта) представлена на Рис. 7.16.
Чтобы получить более четкую картину того как Rails обрабатывает отправку регистрационных данных, давайте поближе познакомимся с user
частью хэша параметров из отладочной информации (Рис. 7.16):
"user" => { "name" => "Foo Bar",
"email" => "foo@invalid",
"password" => "[FILTERED]",
"password_confirmation" => "[FILTERED]"
}
Этот хэш передается в контроллер Users в качестве params
и мы видели, начиная с Раздела 7.1.2, что хэш params
содержит информацию о каждом запросе. В случае URL типа /users/1, значение params[:id]
это id
соответствующего пользователя (1
в этом примере). В случае отправки регистрационной формы, params
вместо этого содержит хэш хэшей, конструкцию, которую мы впервые видели в Разделе 4.3.3, который представил стратегически названную params
переменную в консольной сессии. Эта отладочная информация показывает, что предоставление (отправка) формы дает в результате user
хэш с атрибутами, соответствующими предоставленным значениям, где ключи происходят от name
атрибутов тегов input
которые мы видели в Листинге 7.17; например, значение
<input id="user_email" name="user[email]" type="text" />
с именем "user[email]"
это именно email
атрибут хэша user
.
Хотя хэш-ключи показаны в отладочном выводе в виде строк, в контроллер Users они передаются в виде символов, так что params[:user]
на самом деле является хэшем атрибутов пользователя, именно тех атрибутов, что необходимы в качестве аргумента для User.new
, как мы впервые видели в Разделе 4.4.5 и как представлено в Листинге 7.21. Это означает, что строка
@user = User.new(params[:user])
практически эквивалентна
@user = User.new(name: "Foo Bar", email: "foo@invalid",
password: "foo", password_confirmation: "bar")
В предыдущих версиях Rails использование
@user = User.new(params[:user])
фактически работало, но было по умолчанию небезопасным, требовало особой тщательности при осуществлении довольно глючной процедуры имеющей своей целью защитить базу данных приложения от зловредных пользователей. В Rails 4.0 этот код вызывает ошибку (как показано на Рис. 7.15 и Рис. 7.16 выше), что означает что он безопасен по умолчанию. Мы можем еще раз убедиться в этом проверив что соответствующие тесты не проходят:
$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with invalid information"
7.3.2 Строгие параметры
В Разделе 4.4.5 мы коротко упомянули об идее массового назначения, которая подразумевает инициализацию Ruby-переменной с хэшем значений:
@user = User.new(params[:user]) # Not the final implementation!
Комментарий включенный в Листинг 7.21 и воспроизведенный выше указывает на то что это не финальная реализация. Причина по которой иниацилизация всего хэша params
является очень опасной затеей заключается в том что это приводит к передаче в User.new
всех данных отправленных пользователем. В частности, предположим что, в дополнение к текущим атрибутам, модель User включает в себя атрибут admin
используемый для идентификации пользователей-администраторов сайта. (Мы реализуем именно такой атрибут в Разделе 9.4.1.) Способом присвоить этому атрибуту значение true
является передача значения admin=’1’
как части params[:user]
, этого легко достичь с помощью такого консольного HTTP клиента как curl. Таким образом, передавая весь хэш params
в User.new
, мы тем самым позволяем любому пользователю сайта получить права администратора включив admin=’1’
в веб-запрос.
Для решения этой проблемы предыдущие версии Rails использовали метод attr_accessible
на уровне модели, но в Rails 4.0 более предпочтительной является техника с использованием так называемых строгих параметров на уровне контроллера. Это позваляет нам прописать какие именно параметры являются обязательными, а какие разрешенными. К тому же, передача чистого хэша params
(как это было показано выше) приведет к ошибке, т.е. теперь Rails приложения невосприимчивы к уязвимостям массового назначиния по умолчанию.
В данном случае мы хотим требовать от хэша params
наличие атрибута :user
и мы хотим позволить наличие атрибутов name, email, password и password_confirmation (но только их). Мы можем достигнуть этого следующим образом:
params.require(:user).permit(:name, :email, :password, :password_confirmation)
Этот код вернет версию хэша params
содержащего только разрешенные атрибуты (при этом будет вызвана ошибка если отсутствует атрибут :user
).
Для облегчения использования этих параметров, обычно вводят вспомогательный метод с названием user_params
который возвращает соответствующий инициализационных хэш используемый вместо params[:user]
:
@user = User.new(user_params)
Поскольку user_params
будет использоваться только внутри контроллера Users и нет никакой надобности открывать к нему доступ внешним пользователям через веб, мы сделаем его приватным используя ключевое слово private
, как это показано в Листинге 7.22. (Мы более детально обсудим private
в Разделе 8.2.1.)
create
. app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
# Handle a successful save.
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
С кодом из Листинга 7.22 тесты отправки невалидных данных должны пройти:
$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with invalid information"
7.3.2 Сообщения об ошибках при регистрации
В качестве финального шага в реализации провального создания пользователя мы добавим полезные сообщения об ошибках для указания на проблемы которые помешали регистрации. Rails автоматически предоставляет такие сообщения, основываясь на валидациях модели User. Рассмотрим, например, попытку сохранения пользователя с неправильным адресом электронной почты и коротким паролем:
$ rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?> password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]
Здесь объект errors.full_messages
(который мы видели кратко в Разделе 6.2.2) содержит массив сообщений об ошибках.
Как и в консольной сессии выше, сбой сохранения в Листинге 7.21 генерирует список сообщений об ошибках, связанных с объектом @user
. Для отображения сообщения в браузере, мы рендерим партиал error_messages на странице user new
как это показано в Листинге 7.23. (Написание теста на сообщения об ошибках это хорошая идея и мы оставим эти тесты в качестве упражнения; см. Раздел 7.6.) Стоит отметить что этот партиал сообщений об ошибках лишь первая попытка; конечная версия представлена в Разделе 10.3.2.
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
.
.
.
<% end %>
</div>
</div>
Заметим здесь, что мы render
партиал ’shared/error_messages’
; что отражает общую конвенцию Rails, которая предписывает размещение частичных шаблонов которые мы планируем использовать во многих контроллерах в специально отведенном каталоге shared/
. Это означает, что мы должны создать этот новый каталог вместе с файлом партиала _error_messages.html.erb
. Сам партиал представлен в Листинге 7.24.
app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-error">
The form contains <%= pluralize(@user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li>* <%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
Этот частичный шаблон вводит несколько новых Rails и Ruby конструкций, в том числе два метода для объекта ошибок Rails. Первый метод это count
, который просто возвращает количество ошибок:
>> user.errors.count
=> 2
Другой новый метод это any?
, который (совместно с empty?
) является одним из пары взаимодополняющих методов:
>> user.errors.empty?
=> false
>> user.errors.any?
=> true
Мы видим здесь что метод empty?
, который мы впервые увидели в Разделе 4.2.3 в контексте строк, также работает на объекте ошибок Rails, возваращая true
для пустого объекта и false
в противном случае. Метод any?
это просто противоположность empty?
, возвращающая true
если существует какой-нибудь элемент и false
в противном случае. (Кстати, все эти методы — count
, empty?
и any?
— работают и на массивах Ruby. Мы найдем хорошее применение этому факту в Разделе 10.2.)
Другой новой идеей является текстовый хелпер pluralize
. По умолчанию, он недоступен в консоли, но мы можем явно его включить через модуль ActionView::Helpers::TextHelper
:9
>> include ActionView::Helpers::TextHelper
>> pluralize(1, "error")
=> "1 error"
>> pluralize(5, "error")
=> "5 errors"
Мы видим здесь, что pluralize
принимает целочисленный аргумент и возвращает число с правильной версией множественного числа его второго аргумента. В основе этого метода лежит мощный инфлектор, который знает как преобразовать во множественное число огромное количество слов (в том числе, многие с неправильным множественным числом):
>> pluralize(2, "woman")
=> "2 women"
>> pluralize(3, "erratum")
=> "3 errata"
В результате код
<%= pluralize(@user.errors.count, "error") %>
возвращает "0 errors"
, "1 error"
, "2 errors"
и т.д., в зависимости от количества ошибок, тем самым позволяя нам избежать грамматически неверных фраз вроде "1 errors"
.
Обратите внимание: Листинг 7.24 включает CSS id error_explanation
для использования в стилизации сообщений об ошибках. (Напомним из Раздела 5.1.2 что CSS использует знак решетки #
для стилизации id.) Кроме того, Rails автоматически помещает поля с ошибками в div
ы с CSS классом field_with_errors
. Эти метки затем позволят нам отредактироваь стиль сообщений об ошибках с SCSS показанным в Листинге 7.25, который использует Sass функцию @extend
для включения функциональности двух классов Bootstrap control-group
и error
. В результате чего, при провальной регистрации сообщения об ошибках окружены красным как это показано на Рис. 7.17. Поскольку сообщения генерируются валидациями модели, они автоматически изменятся, если вы когда-нибудь поменяете свое мнение о, скажем, формате адресов электронной почты или минимальной длине паролей.
app/assets/stylesheets/custom.css.scss
.
.
.
/* forms */
.
.
.
#error_explanation {
color: #f00;
ul {
list-style: none;
margin: 0 0 18px 0;
}
}
.field_with_errors {
@extend .control-group;
@extend .error;
}
Для того чтобы увидеть результаты нашей работы в этом разделе, мы повторим шаги из теста провальной регистрации Листинга 7.16 посетив страницу регистрации и кликнув по “Sign up” с пустыми полями ввода. Результат показан на Рис. 7.18. Как вы догадываетесь, глядя на рабочую страницу, в этой точке соответствующий тест тоже должен пройти:
$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with invalid information"
7.4 Успешная регистрация
Получив обработку отправки невалидной формы, теперь пришло время для завершения регистрационной формы, на самом деле сохранив нового пользователя (если валидный) в базу данных. Во-первых, мы попробуем сохранить пользователя; если сохранение пройдет успешно, информация пользователя будет записана в базу данных автоматически, а затем мы перенаправим браузер на страницу профиля пользователя (с дружеским приветствием), как это показано на наброске Рис. 7.19. Если это не удастся, мы просто отступим к сценарию, разработанному в Разделе 7.3.
7.4.1 Завершенная форма регистрации
Для того чтобы закончить работу с формой регистрации, нам необходимо заполнить закомментированный раздел в Листинге 7.21 соответствующим поведением. Сейчас тесты для отправки валидной формы должны быть провальными:
$ bundle exec rspec spec/requests/user_pages_spec.rb \
> -e "signup with valid information"
Это происходит из-за того что дефолтным поведением для Rails-действий является рендеринг соответствующего представления, но здесь нет (и не должно быть) представления соответствующего действию create
. Вместо этого мы должны перенаправить на другую страницу и будет вполне логично если этой страницей будет профиль вновь созданного пользователя. Тестирование того что рендерится правильная страница оставлено в качестве упражнения (Раздел 7.6); код представлен в Листинге 7.26.
create
с сохранением и переадресацией. app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
Обратите внимание на то что мы можем опустить user_path
в редиректе, написав просто redirect_to @user
для перенаправления на страницу показывающую пользователя.
С кодом в Листинге 7.26 наша регистрационная форма отлично работает, что вы можете проверить запустив набор тестов:
$ bundle exec rspec spec/
7.4.2 Флэш
Перед отправкой валидной регистрации в браузер мы собираемся немного отполировать ее, в соответствии с общепринятой в веб приложениях идеей: добавив сообщение, которое временно появляется на следующей странице (в данном случае, приветствуя нашего нового пользователя), а затем исчезает либо при посещении следующей страницы либо при перезагрузке текущей. Rails-способ сделать это состоит в использовании специальной переменной flash с которой мы можем обращаться как с обычным хэшем. Вы можете даже вспомнить консольный пример из Разделе 4.3.3, где мы видели как для перебора хэша использовался стратегически именованный хэш flash
:
$ rails console
>> flash = { success: "It worked!", error: "It failed." }
=> {:success=>"It worked!", error: "It failed."}
>> flash.each do |key, value|
?> puts "#{key}"
?> puts "#{value}"
>> end
success
It worked!
error
It failed.
Мы можем организовать отображение содержимого флэш повсеместно на сайте, включив его в шаблон нашего приложения, как это показно в Листинге 7.27. (Этот код представляет из себя особенно уродливую комбинацию HTML и ERb; упражнение в Разделе 7.6 показывает как сделать его более красивым.)
flash
в шаблон сайта. app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
.
.
.
<body>
<%= render 'layouts/header' %>
<div class="container">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>"><%= value %></div>
<% end %>
<%= yield %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
.
.
.
</body>
</html>
Код в Листинге 7.27 организует вставку каждого флэш элемента в div
тег, с CSS классом, указывающим на тип сообщения. Например, если flash[:success] = "Welcome to the Sample App!"
, то код
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>"><%= value %></div>
<% end %>
произведет такой HTML:
<div class="alert alert-success">Welcome to the Sample App!</div>
(Обратите внимание: ключ :success
является символом, но встроенный Ruby автоматически конвертирует его в строку "success"
перед вставкой в шаблон.) Причина, по которой мы перебираем все возможные пары ключ/значение заключается в том, что благодаря этому мы сможем включать другие виды флэш сообщений. Например, в Разделе 8.1.5 мы увидим flash[:error]
используемое для индикации неудачной попытки входа на сайт.10
Написание теста на правильное флэш сообщение оставлено в качестве упражнения (Раздел 7.6) и мы можем получить прохождение теста назначив flash[:success]
приветственное сообщение в действии create
как это показано в Листинге 7.28.
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
7.4.3 Первая регистрация
Вы можете увидеть результат всей этой работы, зарегистрировав нашего первого пользователя под именем “Rails Tutorial” и с адресом электронной почты “[email protected]”. Получившаяся в результате страница (Рис. 7.20) показывает дружеское сообщение после успешной регистрации, включая приятный зеленый стиль для класса success
, который был добавлен CSS фреймворком Bootstrap из Раздела 5.1.2. (Если вместо этого вы получили сообщение об ошибке указывающее на то что адрес электронной почты уже занят, убедитесь что вы выполнили db:reset
Rake-задачу как это предлагалось сделать в Разделе 7.2.) Затем, после перезагрузки страницы профиля пользователя, флэш сообщение исчезает, как и было обещано (Рис. 7.21).
Теперь мы можем проверить нашу базу данных просто для того чтобы еще раз убедиться что новый пользователь действительно создан:
$ rails console
>> User.find_by(email: "[email protected]")
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]",
created_at: "2013-03-12 05:51:34", updated_at: "2013-03-12 05:51:34",
password_digest: "$2a$10$A58/j7wwh3aAffGkMAO9Q.jjh3jshd.6akhDKtchAz/R...">
7.4.4 Развертывание приложения на сервере с SSL
Мы разработали модель User и функционал регистрации, пришло время развернуть пример приложения на сервере. (Если вы еще не выполнили шаги из введения к Главе 3, вам следует вернуться и сделать это сейчас.) В рамках развертывания мы добавим Secure Sockets Layer (SSL)11 к продакшен приложению, тем самым обезопасив регистрацию. Поскольку мы будем реализовывать SSL на стороне сервера, пример приложения будет также обезопасен для входа пользователя (Глава 8) и также будет устойчив к уязвимости перехвата сессии (Раздел 8.2.1).
В качестве подготовки к развертыванию, вам следует объединить ваши изменения с master
веткой:
$ git add .
$ git commit -m "Finish user signup"
$ git checkout master
$ git merge sign-up
Для того чтобы задеплоить приложение, нам для начала нужно добавить строку инициирующую использование SSL в продакшен. Получившийся в результате конфигурационный файл продакшен окружения config/environments/production.rb
представлен в Листинге 7.29.
config/environments/production.rb
SampleApp::Application.configure do
.
.
.
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true
.
.
.
end
Для того чтобы получить рабочий продакшен сайт, мы должны закоммитить изменения в конфигурационном файле и отправить результат на Heroku:
$ git commit -a -m "Add SSL in production"
$ git push heroku
Затем нам нужно запустить миграцию на продакшен базе данных для того чтобы сообщить Heroku о модели данных User:
$ heroku run rake db:migrate
(Вы можете увидеть deprecation предупреждения в этой точке; просто игнорируйте их.)
Наконец, нам необходимо установить SSL на удаленном сервере. Конфигурирование продакшен сайта для использования SSL это довольно неприятная процедура, кроме всего прочего подразумевающая покупку SSL сертификата для вашего домена. К счастью, для приложений запущенных на домене Heroku (таких как наш Пример Приложения), мы можем упасть на хвост SSL сертификату Heroku. Если вы хотите запустить SSL на собственном домене, таком как example.com
, вам ничего не остается кроме как немного помучиться, о том как это правильно делать вы можете прочитать на Heroku странице о SSL.
Результатом всей этой работы является рабочая форма регистрации на продакшен сервере (Рис. 7.22):
$ heroku open
Обратите внимание на https:// вместо обычного http:// (Рис. 7.22). Дополнительная ‘s’ это указание на то что SSL работает.
Теперь вы можете посетить страницу регистрации и создать нового пользователя. Если возникнут какие-то проблемы, попробуйте выполнить
$ heroku logs
для того чтобы отловить ошибку с помощью логов Heroku.
7.5 Заключение
Возможность регистрировать пользователей это важная веха для нашего приложения. Хотя пример приложения до сих пор не делает ничего полезного, мы заложили необходимый фундамент для последующей разработки. В Главе 8 мы завершим наш механизм аутентификации позволив пользователям входить и выходить из приложения. В Главе 9 позволим пользователям обновлять информацию в их учетных записях и позволим администраторам сайта удалять пользователей тем самым завершив полный набор REST действий ресурса Users из Таблицы 7.1. Наконец, мы добавим методы авторизации к нашим действиям для того чтобы обеспечить безопасность сайта.
7.6 Упражнения
- Проверьте что код в Листинге 7.30 позволяет хелперу
gravatar_for
, определенному в Разделе 7.1.4 принимать опциональный параметрsize
, позволив код вродеgravatar_for user, size: 40
в представлении. - Напишите тесты для ошибок регистрации реализованных в Листинге 7.23. Стартовые рекомендации представлены в Листинге 7.31.
- Написав вначале тест или намеренно ломая, а затем исправляя код приложения, проверьте что тесты в Листинге 7.32 правильно описывают поведение после сохранения пользователя в действии
create
. Листинг 7.32 использует метовhave_selector
представленный в упражнениях Главы 5 (Раздел 5.6). В данном случае мы используемhave_selector
для выбора определенных CSS классов вместе с конкретными HTML тегами. - Как было отмечено ранее, HTML флэша в Листинге 7.27 уродлив. Проверьте, запустив набор тестов, что очищенный код в Листинге 7.33, использующий Rails хелпер
content_tag
, тоже работает.
:size
для хелпера gravatar_for
. app/helpers/users_helper.rb
module UsersHelper
# Returns the Gravatar (http://gravatar.com/) for the given user.
def gravatar_for(user, options = { size: 50 })
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
size = options[:size]
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
spec/requests/user_pages_spec.rb
.
.
.
describe "signup" do
before { visit signup_path }
.
.
.
describe "with invalid information" do
.
.
.
describe "after submission" do
before { click_button submit }
it { should have_title('Sign up') }
it { should have_content('error') }
end
.
.
.
create
. spec/requests/user_pages_spec.rb
.
.
.
describe "with valid information" do
.
.
.
describe "after saving the user" do
before { click_button submit }
let(:user) { User.find_by(email: '[email protected]') }
it { should have_title(user.name) }
it { should have_selector('div.alert.alert-success', text: 'Welcome') }
end
.
.
.
flash
в шаблоне сайта использующий content_tag
. app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
.
.
.
<% flash.each do |key, value| %>
<%= content_tag(:div, value, class: "alert alert-#{key}") %>
<% end %>
.
.
.
</html>
- Mockingbird не поддерживает кастомные изображения вроде фото на Рис. 7.1; я добавил его вручную с помощью Adobe Fireworks. ↑
- Гиппопотам был взят с http://www.flickr.com/photos/43803060@N00/24308857/. ↑
- Rails
debug
информация показана как YAML (a recursive acronym (или рекурсивный акроним)) обозначающий “YAML Ain’t Markup Language”), который является дружественным форматом данных, разработанным так, чтобы быть удобочитаемым и для машин и для людей. ↑ - Вы также можете определять собственные окружения; см. RailsCast on adding an environment. ↑
- Это означает что маршрутизация работает, но соответствующие страницы необязательно должны работать в этой точке. Например, /users/1/edit правильно направляется к
edit
действию контроллера Users, но поскольку действиеedit
пока не существует, обращение к этому URL вернет ошибку. ↑ - В индуизме, аватар это форма проявление божества в человеке или животном. В более широком смысле термин avatar обычно используется для обозначения персонализации пользователя, особенно в виртуальной среде. Но вы уже видели фильм, и почти наверняка знаете об этом. ↑
- Если вашему приложению потребуется обработка кастомных изображений или загрузки других файлов, я рекомендую использовать для этих целей гем Paperclip. ↑
- Странно, не правда ли? Я тоже этого не понимаю. ↑
- Я выяснил это путем поиска
pluralize
в Rails API. ↑ - На самом деле мы будем использовать тесно связанный
flash.now
, но мы отложим эти тонкости до тех пор пока они нам не понадобятся. ↑ - Технически, SSL это теперь TLS, от Transport Layer Security, но все кого я знаю по-прежнему говорят “SSL”. ↑