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):
* Весь код написан Майклом Хартлом. До тех пор пока вы осознаете это,
* вы можете делать с ним все что захотите. Если мы когда нибудь
* встретимся, и если это того стоило, вы можете купить мне
* пиво в ответ.
* ----------------------------------------------------------------------------
*/
Глава 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
Тесты для формы редактирования пользователя аналогичны тестам для формы создания нового пользователя в Листинге 7.31 из упражнений Главы 7, которые добавляют тест для сообщения об ошибке при отправке неверных данных. Результат представлен в Листинге 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.
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).
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
.
Посмотрев на исходный HTML для Рис. 9.2, как и ожидалось, мы видим тег формы (Листинг 9.4).
<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).)
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.
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).
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).
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/
9.1.3 Успешное редактирование
Теперь пришло время заставить работать форму редактирования. Редактирование профильных изображений уже работает поскольку мы переложили загрузку изображений на Gravatar; мы можем отредактировать граватар, кликнув по ссылке “change”, показанной на Рис. 9.2, как это показано на Рис. 9.4. Давайте заставим работать остальной функционал редактирования пользователя.
Тесты для update
действия похожи на аналогичные для create
действия. Листинг 9.9 показывает как использовать Capybara для заполнения полей формы валидной информацией, а затем тестирует, что результирующее поведение корректно. Это большой кусок кода; посмотрим, сможете ли вы проработать его, опираясь на тесты из Главы 7.
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) { "[email protected]" }
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.
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.
9.2.1 Требование входа пользователей
Поскольку ограничения безопасности для edit
и update
действий идентичны, мы будем обрабатывать их в одном RSpec describe
блоке. Начав с требования входа, наши первоначальные тесты затем проверяют, что не вошедшие пользователи, пытающиеся получить доступ к какому либо из действий, просто перенаправляеются на страницу входа, как показано в Листинге 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.
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.
В этой точке наш набор тестов должен решительно позеленеть:
$ bundle exec rspec spec/
9.2.2 Требование правильного пользователя
Конечно, требования входа пользователей недостаточно; пользователи должны иметь доступ к редактированию только своей информации. Мы можем протестировать это, вначале войдя как неправильный пользователь, а затем обратившись к edit
и update
действиям (Листинг 9.13). Обратите внимание: поскольку мы не используем Capybara для этих тестов (no_capybara: true
), мы используем методы get
и patch
для обращения к действиям edit
и update
напрямую. К тому же, так как пользователи никогда не должны даже пытаться изменить профиль другого пользователя, мы сделаем переадресацию не на страницу входа, а на корневой URL.
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: "[email protected]") }
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: "[email protected]")
Это создает пользователя с адресом электронной почты, отличающимся от дефолтного. Тесты описывают что этот пользователь не должен иметь доступа к действиям edit
или update
оригинального пользователя.
Код приложения добавляет второй предфильтр для вызова метода correct_user
, как это показано в Листинге 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).
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.
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).
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.
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.)
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 мы добавим административный интерфейс к списку пользователей для того чтобы (предположительно проблемные) могли быть удалены.
9.3.1 Список пользователей
Хотя мы сохраним страницы show
отдельных пользователей видимыми для всех посетителей сайта, для страницы user index
будет реализовано ограничение, отображающее ее только для зарегистрированных пользователей, также будет реализовано ограничение того, сколько зарегистрированных пользователей будет отображаться на каждой странице списка. Мы начнем с тестирования того, что действие index
защищено, посетив users_path
(Таблица 7.1) и проверив что мы перенаправлены на страницу входа. Как и с остальными тестами авторизации, мы поместим этот пример в интеграционные тест авторизации, как это показано в Листинге 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.
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.
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: "[email protected]")
FactoryGirl.create(:user, name: "Ben", email: "[email protected]")
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.)
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 прежде чем продолжать.
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).
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) довольно просты.
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
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, он выглядит несколько безлюдно. Давайте исправим эту печальную ситуацию.
9.3.2 Образцы пользователей
В этом разделе мы дадим нашему одинокому образцу пользователя небольшую компанию. Конечно, чтобы создать достаточное количество пользователей, мы могли бы, использовать наш веб-браузер, для посещения страницы регистрации и создания новых пользователей по одному, но куда более продвинутым решением является использование Ruby (и Rake), чтобы сделать пользователей для нас.
Во-первых, мы добавим Faker гем в Gemfile
, который позволит нам делать образцы пользователей с полу-реалистичными именами и адресами электронной почты (Листинг 9.28).
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. (Это довольно продвинутый материал, так что не особо заморачивайтесь деталями.)
lib/tasks/sample_data.rake
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
User.create!(name: "Example User",
email: "[email protected]",
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.)
9.3.3 Пагинация
Наш оригинальный пользователь более не страдает от одиночества, но теперь у нас появилась другая проблема: у нашего пользователя слишком большая компания, и вся она расположилась на одной странице. Сейчас это сотня, что уже является довольно большим числом, а на реальном сайте это могут быть и тысячи. Решение заключается в пагинации (разбиении на страницы, постраничном выводе) пользователей, так, чтобы (например) показывать только 30 на каждой странице одновременно.
В Rails есть несколько способов разбиения на страницы, мы будем использовать один из самых простых и надежных, он называется will_paginate. Для того, чтобы использовать его нам необходимо включить сам гем will_paginate, а также гем bootstrap-will_paginate, который конфигурирует will_paginate для использования пагинационных стилей предоставляемых Bootstrap. Обновленный Gemfile
представлен в Листинге 9.30.
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 "[email protected]"
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”, а адресом электронной почты - “[email protected]”, второй пользователь получит имя “Person 2” и адрес электронной почты “[email protected]”, и т.д.. Полный код представлен в Листинге 9.31.
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)
.
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); мы вскоре увидим почему код был добавлен сверху и снизу списка пользователей.
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
сгенерировал автоматически.
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
сверху и снизу списка пользователей, ссылки на страницы появились в обоих местах.
Если теперь кликнуть по любой 2 или Next ссылке, вы получите вторую страницу с результатами, как это показано на Рис. 9.11.
Вам также следует проверить что тесты проходят:
$ bundle exec rspec spec/
9.3.4 Частичный рефакторинг
Разбитый на страницы список пользователей теперь закончен, но есть одно улучшение, от которого я не могу удержаться: Rails имеет несколько невероятно ловких инструментов для создания компактных представлений, и в этом разделе мы займемся рефакторингом страницы со списком пользователей, используя эти инструменты. Так как наш код хорошо протестирован, мы можем с уверенностью приступить к рефакторингу, будучи уверенными в том, что мы вряд ли нарушим функциональность сайта.
Первым шагом нашего рефакторинга будет замена пользовательского li
из Листинга 9.33 на вызов render
(Листинг 9.35).
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).
app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 52 %>
<%= link_to user.name, user %>
</li>
Это явное улучшение, но мы можем сделать еще лучше: мы можем вызвать render
непосредственно на переменную @users
(Листинг 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
действие необходимое для выполнения удаления. Но мы начнем с создания класса уполномоченных на это административных пользователей.
9.4.1 Административные пользователи
Мы будем идентифицировать привилегированных пользователей с правами администратора посредством булевого атрибута admin
в модели User, что, как мы увидим, автоматически приведет нас к методу admin?
для проверки административного статуса. Мы можем написать тесты для этого атрибута, как в Листинге 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.
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, и читателям нашего кода.)
Наконец, мы мигрируем базу данных разработки и подготавливаем тестовую бд:
$ 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).
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: "[email protected]",
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.
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.
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.
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”.
Для того чтобы получить рабочие удаляющие ссылки, нам необходимо добавить действие destroy
(Таблица 7.1) которое будет находить соответствующего пользователя и удалять его с помощью метода Active Record destroy
, по завершении перенаправляя пользователя на страницу списка пользователей, как это показано в Листинге 9.44. Обратите внимание, что мы также добавили :destroy
в предфильтр signed_in_user
.
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).
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.
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. (Необязательные гемы которые могут оказаться системозависимыми закомментированы. Вы можете раскомментировать их для того чтобы посмотреть работают ли они на вашей системе.)
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 Упражнения
- Направив запрос PATCH напрямую к методу
update
как это показано в Листинге 9.48, проверьте что атрибутadmin
не может быть отредактирован через веб. Вначале получите провальный тест и лишь потом добейтесь его прохождения. (Подсказка: Вашим первым шагом должно стать добавлениеadmin
в список разрешенных параметров вuser_params
.) - Организуйте открытие Gravatar ссылки “change” из Листинга 9.3 в новом окне (или вкладке). Подсказка: Ищите в сети; вы должны найти один простой и надежный метод с участием так называмого
_blank
. - Текущий тест аутентификации проверяет что навигационные ссылки, такие как “Profile” и “Settings” появляются когда пользователь входит. Добавьте тест проверяющий что эти ссылки не видны невошедшим пользователям.
- Примените тестовый хелпер
sign_in
из Листинга 9.6 в как можно большем количестве мест. - Удалите дублирующийся код формы отрефакторив
new.html.erb
иedit.html.erb
представления с помощью партиала из Листинга 9.49. Обратите внимание на то, что вам придется явно передать пременную формыf
в виде локальной переменной, как это показано в Листинге 9.50. Вам также понадобится обновить тесты, поскольку формы не останутся в точности такими же; найдите небольшую разницу между ними и обновите тесты соответствующим образом. - Зарегистрированным пользователям совершенно незачем иметь доступ к
new
иcreate
действиям контроллера Users. Организуйте для таких пользователей переадресацию в корневой URL, при попытке обратиться к этим страницам. - Изучите объект
request
вставляя некоторые методы перечисленные в Rails API9 в шаблон сайта. (Обращайтесь к Листингу 7.1 если застрянете.) - Напишите тест для проверки того, что дружелюбная переадресация направляет к данному URL только в первый раз. На последующие попытки входа, адрес перенаправления должени меняться на дефолтный (т.e., страницу профиля). См. подсказку в Листинге 9.51 (и подсказкой в данном случае я называю решение).
- Модифицируйте действие
destroy
так, чтобы предотвратить уничтожение административными пользователями самих себя. (Начните с написания теста.)
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
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 %>
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>
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
- Изображение взято на http://www.flickr.com/photos/sashawolff/4598355045/. ↑
- Сайт Gravatar на самом деле переадресует на http://en.gravatar.com/emails, предназначенную для англоговорящих пользователей, но я опустил en часть для возможного использования других языков. ↑
- Не беспокойтесь о том, как это работает, детали представляют интерес для разработчиков самого фреймворка Rails, но, по замыслу, не имеют значения для разработчиков Rails приложений. ↑
- Код в этом разделе это адаптация (переделка) гема Clearance команды разработчиков thoughtbot. ↑
- Спасибо пользователю Yoel Adler и за указание на этот нюанс и за поиск решения. ↑
- Фотография ребенка из http://www.flickr.com/photos/glasgows/338937124/. ↑
- Имя
user
непринципиально — мы могли бы написать@users.each do |foobar|
, а затем использоватьrender foobar
. Ключом является класс объекта — в данном случае,User
. ↑ - Инструменты командной строки, такие как curl могут выдавать PATCH запросы такой формы. ↑
- http://api.rubyonrails.org/v4.0.0/classes/ActionDispatch/Request.html ↑