В этой главе мы завершим REST действия для ресурса Users (Таблица 7.1, лекция 7) добавив edit, update, index, и destroy действия. Мы начнем с того, что дадим пользователям возможность обновлять свои профили, что также обеспечит естественную возможность для обеспечения модели безопасности (стало возможным, благодаря аутентификационному коду из Главы 8). Затем мы сделаем список всех пользователей (также требует авторизации), что будет поводом для внедрения образцов данных и постраничного вывода (пагинации). Наконец, мы также добавим возможность удалять пользователей, стиранием их в базе данных. Так как мы не можем позволить любому пользователю обладать такими опасными возможностями, мы позаботимся о создании привилегированного класса административных пользователей (администраторов), авторизованных для удаления других пользователей.
Мы начнем с создания отдельной ветки updating-users:
$ git checkout -b updating-users
Основная идея редактирования информации о пользователе тесно параллельна созданию новых пользователей (Глава 7). Вместо new действия визуализирующего представление для нового пользователя, мы имеем edit действие, отображающее представление для редактирования пользователей; вместо create отвечающего на запрос POST, мы имеем update действие, отвечающее на запрос PATCH (Блок 3.3). Основное отличие заключается в том, что зарегистрироваться может любой человек, но только текущий пользователь должен иметь возможность обновлять свою информацию. Это означает, что нам необходимо обеспечить контроль доступа таким образом, чтобы только авторизированные пользователи могли редактировать и обновлять информацию; аутентификационный механизм из Главы 8 позволит нам использовать предфильтр для обеспечения этого вида контроля
Мы начнем с формы редактирования, набросок которой представлен на рис 9.1.
(рис 9.1) Набросок страницы для редактирования пользователя.
Тесты для формы редактирования пользователя аналогичны тестам для формы создания нового пользователя в Листинге 7.31 из упражнений Главы 7, которые добавляют тест для сообщения об ошибке при отправке неверных данных. Результат представлен в Листинге 9.1.
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, лекции 7 указывает на то, что правильным URL для страницы редактирования пользователя является /users/1/edit (предполагается что id пользователя равен 1). Вспомните что id пользователя доступен в переменной params[:id], а это означает что мы можем найти пользователя с помощью кода в Листинге 9.2.
class UsersController < ApplicationController
.
.
.
def edit
@user = User.find(params[:id])
end
.
.
.
end
Для прохождения этих тестов требуется создание соответствующего (edit) представления, показанного в Листинге 9.3. Обратите внимание, как сильно оно походит на представление new user из Листинга 7.17 лекции 7; большое перекрытие предполагает факторинг повторяющгося кода в партиал, который мы оставим в качестве упражнения (Раздел 9.6).
<% 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.
(рис 9.2) Начальная страница редактирования пользователя с предзаполненными полями Name Email.
Посмотрев на исходный 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, лекции 7, Rails подделывает иx с помощью POST запроса и скрытого поля
Стоит также упомянуть здесь еще одну тонкость: код 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).)
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.
.
.
.
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, лекции 7, совместно с удобным вспомогательным методом current_user определенным в Листинге 8.22:
<%= link_to "Settings", edit_user_path(current_user) %>
Полный код приложения представлен в Листинге 9.7).
<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. Код приложения создает действие update которое использует update_attributes (Раздел 6.1.5) для обновления пользователя на основе отправленного хэша params, как это показано в Листинге 9.8. С невалидной информацией, попытка обновления вернет false и ветка else заново отрендерит страницу редактирования. Мы видели этот способ ранее; структура очень похожа на первую версию действия create (Листинге 7.21).
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.3) Сообщение об ошибке после отправления формы обновления.
Теперь пришло время заставить работать форму редактирования. Редактирование профильных изображений уже работает поскольку мы переложили загрузку изображений на Gravatar; мы можем отредактировать граватар, кликнув по ссылке "change", показанной на рис 9.2, как это показано на рис 9.4. Давайте заставим работать остальной функционал редактирования пользователя.
(рис 9.4) Интерфейс обрезки изображений Gravatar с фоткой какого-то чувака.
Тесты для update действия похожи на аналогичные для create действия. Листинг 9.9 показывает как использовать Capybara для заполнения полей формы валидной информацией, а затем тестирует, что результирующее поведение корректно. Это большой кусок кода; посмотрим, сможете ли вы проработать его, опираясь на тесты из Главы 7.
require 'spec_helper'
describe "User pages" do
.
.
.
describe "edit" do
let(:user) { FactoryGirl.create(:user) }
before do
sign_in user
visit edit_user_path(user)
end
.
.
.
describe "with valid information" do
let(:new_name) { "New Name" }
let(:new_email) { "new@example.com" }
before do
fill_in "Name", with: new_name
fill_in "Email", with: new_email
fill_in "Password", with: user.password
fill_in "Confirm Password", with: user.password
click_button "Save changes"
end
it { should have_title(new_name) }
it { should have_selector('div.alert.alert-success') }
it { should have_link('Sign out', href: signout_path) }
specify { expect(user.reload.name).to eq new_name }
specify { expect(user.reload.email).to eq new_email }
end
end
end
Обратите внимание на то что Листинг 9.9 добавляет метод sign_in из Листинга 9.6 в блок before, что необходимо для прохождения тестов ссылки "Sign out" и также предполагает защиту действия edit от невошедших пользователей (Раздел 9.2.1).
Единственной новинкой в Листинг 9.9 является метод reload, который появляется в тесте для изменения атрибутов пользователя:
specify { expect(user.reload.name).to eq new_name }
specify { expect(user.reload.email).to eq new_email }
Этот код перезагружает user переменную из (тестовой) базы данных используя user.reload, а затем проверяет, что новые имя пользователя и email совпадают с новыми значениями.
Действие update, необходимое для прохождения тестов в Листинге 9.9 аналогично финальной форме create действия (Листин 8.27), как видно в Листинг 9.10. Единственное что он делает, это добавляет
flash[:success] = "Profile updated" redirect_to @user
к коду в Листинг 9.8.
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/
Одним из приятных эффектов построения механизма аутентификации в Главе 8 является то, что мы теперь готовы к реализации авторизации: аутентификация позволяет нам идентифицировать пользователей нашего сайта, а авторизация позволяет нам контролировать предоставляемые им возможности.
Хотя edit и update действия из Раздела 9.1 функционально завершены, они страдают от нелепой бреши в безопасности: они позволяют любым (даже незарегистрированным) пользователям иметь доступ к любому действию, и любой зарегистрированный пользователь может изменять информацию любого другого пользователя. В этом разделе мы реализуем модель безопасности, которая будет требовать от пользователей входа в систему и будет предотвращать обновление любой информации, кроме их собственной. Незарегистрированные пользователи, пытающиеся получить доступ к защищенным страницам будут перенаправлены на страницу входа с полезным сообщением, как это показано на рис 9.5.
(рис 9.5) Набросок результата посещения защищенной страницы
Поскольку ограничения безопасности для edit и update действий идентичны, мы будем обрабатывать их в одном RSpec describe блоке. Начав с требования входа, наши первоначальные тесты затем проверяют, что не вошедшие пользователи, пытающиеся получить доступ к какому либо из действий, просто перенаправляеются на страницу входа, как показано в Листинге 9.11.
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, лекция 7). Это необходимо из-за того, что браузер не может посетить непосредственно само действие 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.
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.)
Совместно с .
(рис 9.6) Форма входа после попытки получить доступ к защищенной странице.
В этой точке наш набор тестов должен решительно позеленеть:
$ bundle exec rspec spec/
Конечно, требования входа пользователей недостаточно; пользователи должны иметь доступ к редактированию только своей информации. Мы можем протестировать это, вначале войдя как неправильный пользователь, а затем обратившись к edit и update действиям (Листинг 9.13). Обратите внимание: поскольку мы не используем Capybara для этих тестов (no_capybara: true), мы используем методы get и patch для обращения к действиям edit и update напрямую. К тому же, так как пользователи никогда не должны даже пытаться изменить профиль другого пользователя, мы сделаем переадресацию не на страницу входа, а на корневой URL.
require 'spec_helper'
describe "Authentication" do
.
.
.
describe "authorization" do
.
.
.
describe "as wrong user" do
let(:user) { FactoryGirl.create(:user) }
let(:wrong_user) { FactoryGirl.create(:user, email: "wrong@example.com") }
before { sign_in user, no_capybara: true }
describe "submitting a GET request to the Users#edit action" do
before { get edit_user_path(wrong_user) }
specify { expect(response.body).not_to match(full_title('Edit user')) }
specify { expect(response).to redirect_to(root_url) }
end
describe "submitting a PATCH request to the Users#update action" do
before { patch user_path(wrong_user) }
specify { expect(response).to redirect_to(root_url) }
end
end
end
end
Обратите внимание на то, что фабрика может принимать опцию:
FactoryGirl.create(:user, email: "wrong@example.com")
Это создает пользователя с адресом электронной почты, отличающимся от дефолтного. Тесты описывают что этот пользователь не должен иметь доступа к действиям edit или update оригинального пользователя.
Код приложения добавляет второй предфильтр для вызова метода correct_user, как это показано в Листинге 9.14.
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).
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/
Защита наших страниц закончена, как и было написано, но есть один небольшой недостаток: когда пользователи пытаются получить доступ к защищенной странице, они перенаправляются к странице профиля, независимо от того, куда они пытались попасть. Другими словами, если не залогинившийся пользователь пытается посетить страницу редактирования, после входа пользователь будет перенаправлен на /users/1 вместо /users/1/edit. Было бы намного более доброжелательно, по отношению к пользователям, все же перенаправлять их на запрашиваемую страницу вместо этого .
Для того чтобы протестировать такую "дружелюбную переадресацию", мы вначале посещаем страницу редактирования пользователя, которая перенаправит нас на страницу входа. Затем мы введем валидную информацию для входа и кликнем по кнопке "Sign in". Результирующей страницей, которой по дефолту является профиль пользователя, в данном случае должна быть страница "Edit user". Тест для этой последовательности представлен в Листинге 9.16.
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
Теперь реализация.store_location и redirect_back_or, определенных в хелпере Sessions (Листинг 9.17).
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
В роли механизма хранения выступает объект
Для того чтобы использовать store_location, нам необходимо добавить ее в предфильтр signed_in_user, как это показано в Листинге 9.18.
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.)
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.7) Набросок списка пользователей с пагинацией и навигационной ссылкой “Users”.
Хотя мы сохраним страницы show отдельных пользователей видимыми для всех посетителей сайта, для страницы user index будет реализовано ограничение, отображающее ее только для зарегистрированных пользователей, также будет реализовано ограничение того, сколько зарегистрированных пользователей будет отображаться на каждой странице списка. Мы начнем с тестирования того, что действие index защищено, посетив users_path (Таблица 7.1, лекция 7) и проверив что мы перенаправлены на страницу входа. Как и с остальными тестами авторизации, мы поместим этот пример в интеграционные тест авторизации, как это показано в Листинге 9.20.
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.
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.
require 'spec_helper'
describe "User pages" do
subject { page }
describe "index" do
before do
sign_in FactoryGirl.create(:user)
FactoryGirl.create(:user, name: "Bob", email: "bob@example.com")
FactoryGirl.create(:user, name: "Ben", email: "ben@example.com")
visit users_path
end
it { should have_title('All users') }
it { should have_content('All users') }
it "should list each user" do
User.all.each do |user|
expect(page).to have_selector('li', text: user.name)
end
end
end
.
.
.
end
Как вы можете вспомнить из соответствующего действия в 'demo app' (Листинг 2.4), код приложения использует User.all для вытягивания всех пользователей из базы данных, присваивая их переменной экземпляра @users для использования в представлении, как это показано в Листинге 9.23. (Если отображение всех пользователей за раз кажется вам плохой идеей, вы правы и мы избавимся от этого недостатка в Разделе 9.3.3.)
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 прежде чем продолжать.
<% 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).
.
.
.
/* 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;
}
}
}
.
.
.
/* 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 лекции 7. Тест (Листинг 9.26) и код приложения (Листинг 9.27) довольно просты.
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
<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.8) Страница списка пользователей /users с одним пользователем.
В этом разделе мы дадим нашему одинокому образцу пользователя небольшую компанию. Конечно, чтобы создать достаточное количество пользователей, мы могли бы, использовать наш веб-браузер, для посещения страницы регистрации и создания новых пользователей по одному, но куда более продвинутым решением является использование Ruby (и Rake), чтобы сделать пользователей для нас.
Во-первых, мы добавим Faker гем в Gemfile, который позволит нам делать образцы пользователей с полу-реалистичными именами и адресами электронной почты (Листинг 9.28).
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. (Это довольно продвинутый материал, так что не особо заморачивайтесь деталями.)
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
end
end
Этот код определяет задачу db:populate которая создает образец пользователя с именем и адресом электронной почты, делая реплику нашего предыдущего пользователя, после чего делает еще 99 экземпляров. Строка
task populate: :environment do
обеспечивает Rake задаче доступ к локальному Rails окружению, включая модель User (и, следовательно, к User.create!). Здесь create! это метод очень похожий на create, за той лишь разницей, что он вызывает исключение (Раздел 6.1.4) при неудачном создании, вместо того чтобы тихо возвращать false. Эта крикливая конструкция упрощает отладку, помогая избежать тихих ошибок.
С пространством имен :db как в Листинге 9.29, мы можем вызвать Rake задачу следующим образом:
$ bundle exec rake db:reset $ bundle exec rake db:populate $ bundle exec rake test:prepare
После запуска Rake задачи, наше приложениее имеет 100 примеров пользователей, как это видно на рис 9.9. (Я взял на себя смелость связать первые несколько образцов адресов с фотографиями так что не все изображения являются дефолтными картинками Gravatar.)
(рис 9.9) Страница списка пользователей /users с 100 образцов пользователей.
Наш оригинальный пользователь более не страдает от одиночества, но теперь у нас появилась другая проблема: у нашего пользователя слишком большая компания, и вся она расположилась на одной странице. Сейчас это сотня, что уже является довольно большим числом, а на реальном сайте это могут быть и тысячи. Решение заключается в пагинации (разбиении на страницы, постраничном выводе) пользователей, так, чтобы (например) показывать только 30 на каждой странице одновременно.
В Rails есть несколько способов разбиения на страницы, мы будем использовать один из самых простых и надежных, он называется will_paginate. Для того, чтобы использовать его нам необходимо включить сам гем will_paginate, а также гем bootstrap-will_paginate, который конфигурирует will_paginate для использования пагинационных стилей предоставляемых Bootstrap. Обновленный Gemfile представлен в Листинге 9.30.
source 'https://rubygems.org' ruby '2.0.0' #ruby-gemset=railstutorial_rails_4_0 gem 'rails', '4.0.2' gem 'bootstrap-sass', '2.3.2.0' gem 'bcrypt-ruby', '3.1.2' gem 'faker', '1.1.2' gem 'will_paginate', '3.0.4' gem 'bootstrap-will_paginate', '0.0.9' . . .
Затем запустите bundle install:
Затем запустите bundle install:
Вам также следует перезагрузить веб-сервер для того чтобы убедиться, что новые гемы были загружены как следует.
Поскольку гем will_paginate широко распространен, нам нет необходимости тестировать его, так что мы можем применить несколько облегченный подход. Во-первых, мы протестируем наличие div с CSS классом "pagination", который выводится гемом will_paginate. Затем мы проверим что на первой странице результатов представлены правильные пользователи. Для этого нам потребуется использовать метод paginate, о котором мы вскоре узнаем подробнее.
Как и прежде, мы будем использовать Factory Girl для имитации пользователей, но мы тут же натыкаемся на проблему: адреса электронной почты пользователей должны быть уникальными, что, как представляется, требует создания более чем 30 пользователей вручную — ужасно муторная работа. К тому же, при тестировании выводимого списка пользователей было бы удобно, если бы у них были разные имена. К счастью, Factory Girl предвидела этот вопрос, и обеспечила последовательность (цикл) для ее решения. Наша оригинальная фабрика (Листинг 7.8) хард-кодила имя и адрес электронной почты:
FactoryGirl.define do
factory :user do
name "Michael Hartl"
email "michael@example.com"
password "foobar"
password_confirmation "foobar"
end
end
Вместо этого мы можем организовать последовательность имен и адресов электронной почты с помощью метода sequence:
factory :user do
sequence(:name) { |n| "Person #{n}" }
sequence(:email) { |n| "person_#{n}@example.com"}
.
.
.
Здесь sequence принимает символ соответствующий выбранному атрибуту (такому как :name) и блок с одной переменной, которую мы назвали n. При последующих вызовах FactoryGirl метод,
FactoryGirl.create(:user)
Переменная блока n автоматически увеличивается, таким образом, именем первого пользователя будет "Person 1", а адресом электронной почты - "person_1@example.com", второй пользователь получит имя "Person 2" и адрес электронной почты "person_2@example.com", и т.д.. Полный код представлен в Листинге 9.31.
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).
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); мы вскоре увидим почему код был добавлен сверху и снизу списка пользователей.
<% 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 сгенерировал автоматически.
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 сверху и снизу списка пользователей, ссылки на страницы появились в обоих местах.
(рис 9.10) Страница списка пользователей /users с пагинацией.
Если теперь кликнуть по любой 2 или Next ссылке, вы получите вторую страницу с результатами, как это показано на рис 9.11.
(рис 9.11) Страница 2 списка пользователей (/users?page=2).
Вам также следует проверить что тесты проходят:
$ bundle exec rspec spec/
Разбитый на страницы список пользователей теперь закончен, но есть одно улучшение, от которого я не могу удержаться: Rails имеет несколько невероятно ловких инструментов для создания компактных представлений, и в этом разделе мы займемся рефакторингом страницы со списком пользователей, используя эти инструменты. Так как наш код хорошо протестирован, мы можем с уверенностью приступить к рефакторингу, будучи уверенными в том, что мы вряд ли нарушим функциональность сайта.
Первым шагом нашего рефакторинга будет замена пользовательского li из Листинга 9.33 на вызов render (Листинг 9.35).
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
Здесь мы вызываем user непринципиально — мы могли бы написать @users.each do |foobar|, а затем использовать render foobar. Ключом является класс объекта — в данном случае, User._user.html.erb, который мы должны создать (Листинг 9.36).
<li> <%= gravatar_for user, size: 52 %> <%= link_to user.name, user %> </li>
Это явное улучшение, но мы можем сделать еще лучше: мы можем вызвать render непосредственно на переменную @users (Листинг 9.37).
<% 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/
Теперь, когда список пользователей завершен, осталось лишь одно каноничное REST действие: destroy. В этом разделе мы добавим ссылки для удаления пользователей, как это показано на 12" type="image" />, и определим destroy действие необходимое для выполнения удаления. Но мы начнем с создания класса уполномоченных на это административных пользователей.
(рис 9.12) Набросок списка пользователей с удаляющими ссылками.
Мы будем идентифицировать привилегированных пользователей с правами администратора посредством булевого атрибута admin в модели User, что, как мы увидим, автоматически приведет нас к методу admin? для проверки административного статуса. Мы можем написать тесты для этого атрибута, как в Листинге 9.38.
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.39" type="example">Листинг 9.39), приводя к модели данных показанной на class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, default: false
end
end
Обратите внимание на то, что мы добавили аргумент Наконец, мы мигрируем базу данных разработки и подготавливаем тестовую бд: Как и ожидалось, Rails догадывается о булевом характере атрибута В результате, тесты для admin должны пройти: В качестве последнего шага, давайте обновим наш заполнитель образцов данных для того, чтобы сделать первого пользователя администратором (Листинг 9.40). Наконец, перезапустим заполнитель для перезагрузки базы данных и последующей перестройки ее с нуля: Возвращение к строгим параметрам Вы могли заметить, что Листинг 9.40 делает пользователя администратором с помощью добавления Этот запрос сделал бы администратором пользователя 17, что могло бы стать потенциально серьезной брешью в безопасности, если не сказать больше. Из-за этой опасности очень важно передавать на дальнейшую обработку только параметры безопасных для редактирования атрибутов. Как отмечалось в Разделе 7.3.2, это может быть достигнуто с помощью строгих параметров вызывая В частности, обратите внимание на то, что default: false к add_column в Листинге 9.39, что означает, что пользователи не будут администраторами по умолчанию. (Без аргумента default: false, admin был бы по умолчанию nil, что все же является false, так что этот шаг не является строго обязательным. Однако это более явно и четко сообщает о наших намерениях, и Rails, и читателям нашего кода.)
(рис 9.13) Модель User с добавленным булевым атрибутом admin.
$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
admin и автоматически добавляет метод со знаком вопроса admin?:$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true
$ bundle exec rspec spec/models/user_spec.rb
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
admin = User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true)
.
.
.
end
end
$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake test:prepare
patch /users/17?admin=1
require и permit на хэше params: def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
admin не включен в список разрешенных атрибутов. Именно это не позволяет произвольному пользователю предоставлять себе административный доступ к нашему приложению. Поскольку это очень важный нюанс, хорошей идеей будет написание теста на каждый нередактируемый атрибут и написание такого теста для атрибута admin оставлено в качестве упражнения (Раздел 9.6).
Последний шаг, необходимый для завершения ресурса Users, заключается в добавлении удаляющих ссылок и destroy действия. Мы начнем с добавления удаляющей ссылки для каждого пользователя на страницу со списком пользователей, при этом ограничив доступ к ним.
Для написания тестов для удаляющего функционала нам пригодится фабрика создающая администраторов. Мы можем достигнуть этого, добавив блок :admin к нашим фабрикам как это показано в Листинг 9.41.
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.
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.
<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".
(рис 9.14) Страница списка пользователей /users с удаляющими ссылками.
Для того чтобы получить рабочие удаляющие ссылки, нам необходимо добавить действие destroy(Таблица 7.1, лекция 7), которое будет находить соответствующего пользователя и удалять его с помощью метода Active Record destroy, по завершении перенаправляя пользователя на страницу списка пользователей, как это показано в Листинге 9.44. Обратите внимание, что мы также добавили :destroy в предфильтр signed_in_user.
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, лекция 7).
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.
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/
Мы прошли долгий путь с момента введения контроллера 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. (Необязательные гемы которые могут оказаться системозависимыми закомментированы. Вы можете раскомментировать их для того чтобы посмотреть работают ли они на вашей системе.)
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
update как это показано в Листинге 9.48, проверьте что атрибут admin не может быть отредактирован через веб. Вначале получите провальный тест и лишь потом добейтесь его прохождения. (Подсказка: Вашим первым шагом должно стать добавление admin в список разрешенных параметров в user_params.)_blank.sign_in из Листинга 9.6 в как можно большем количестве мест.new.html.erb и edit.html.erb представления с помощью партиала из Листинга 9.49. Обратите внимание на то, что вам придется явно передать пременную формы f в виде локальной переменной, как это показано в Листинге 9.50. Вам также понадобится обновить тесты, поскольку формы не останутся в точности такими же; найдите небольшую разницу между ними и обновите тесты соответствующим образом.new и create действиям контроллера Users. Организуйте для таких пользователей переадресацию в корневой URL, при попытке обратиться к этим страницам.destroy так, чтобы предотвратить уничтожение административными пользователями самих себя. (Начните с написания теста.)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
<%= 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 %>
<% 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></example>
<example number="9." title="" type="listing">
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
В этой главе мы завершим REST действия для ресурса Users (Таблица 7.1, лекция 7) добавив edit, update, index, и destroy действия. Мы начнем с того, что дадим пользователям возможность обновлять свои профили, что также обеспечит естественную возможность для обеспечения модели безопасности (стало возможным, благодаря аутентификационному коду из Главы 8). Затем мы сделаем список всех пользователей (также требует авторизации), что будет поводом для внедрения образцов данных и постраничного вывода (пагинации). Наконец, мы также добавим возможность удалять пользователей, стиранием их в базе данных. Так как мы не можем позволить любому пользователю обладать такими опасными возможностями, мы позаботимся о создании привилегированного класса административных пользователей (администраторов), авторизованных для удаления других пользователей.
Мы начнем с создания отдельной ветки updating-users:
$ git checkout -b updating-users
Основная идея редактирования информации о пользователе тесно параллельна созданию новых пользователей (Глава 7). Вместо new действия визуализирующего представление для нового пользователя, мы имеем edit действие, отображающее представление для редактирования пользователей; вместо create отвечающего на запрос POST, мы имеем update действие, отвечающее на запрос PATCH (Блок 3.3). Основное отличие заключается в том, что зарегистрироваться может любой человек, но только текущий пользователь должен иметь возможность обновлять свою информацию. Это означает, что нам необходимо обеспечить контроль доступа таким образом, чтобы только авторизированные пользователи могли редактировать и обновлять информацию; аутентификационный механизм из Главы 8 позволит нам использовать предфильтр для обеспечения этого вида контроля
Мы начнем с формы редактирования, набросок которой представлен на рис 9.1.
(рис 9.1) Набросок страницы для редактирования пользователя.
Тесты для формы редактирования пользователя аналогичны тестам для формы создания нового пользователя в Листинге 7.31 из упражнений Главы 7, которые добавляют тест для сообщения об ошибке при отправке неверных данных. Результат представлен в Листинге 9.1.
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, лекции 7 указывает на то, что правильным URL для страницы редактирования пользователя является /users/1/edit (предполагается что id пользователя равен 1). Вспомните что id пользователя доступен в переменной params[:id], а это означает что мы можем найти пользователя с помощью кода в Листинге 9.2.
class UsersController < ApplicationController
.
.
.
def edit
@user = User.find(params[:id])
end
.
.
.
end
Для прохождения этих тестов требуется создание соответствующего (edit) представления, показанного в Листинге 9.3. Обратите внимание, как сильно оно походит на представление new user из Листинга 7.17 лекции 7; большое перекрытие предполагает факторинг повторяющгося кода в партиал, который мы оставим в качестве упражнения (Раздел 9.6).
<% 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.
(рис 9.2) Начальная страница редактирования пользователя с предзаполненными полями Name Email.
Посмотрев на исходный 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, лекции 7, Rails подделывает иx с помощью POST запроса и скрытого поля
Стоит также упомянуть здесь еще одну тонкость: код 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).)
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.
.
.
.
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, лекции 7, совместно с удобным вспомогательным методом current_user определенным в Листинге 8.22:
<%= link_to "Settings", edit_user_path(current_user) %>
Полный код приложения представлен в Листинге 9.7).
<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. Код приложения создает действие update которое использует update_attributes (Раздел 6.1.5) для обновления пользователя на основе отправленного хэша params, как это показано в Листинге 9.8. С невалидной информацией, попытка обновления вернет false и ветка else заново отрендерит страницу редактирования. Мы видели этот способ ранее; структура очень похожа на первую версию действия create (Листинге 7.21).
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.3) Сообщение об ошибке после отправления формы обновления.
Теперь пришло время заставить работать форму редактирования. Редактирование профильных изображений уже работает поскольку мы переложили загрузку изображений на Gravatar; мы можем отредактировать граватар, кликнув по ссылке "change", показанной на рис 9.2, как это показано на рис 9.4. Давайте заставим работать остальной функционал редактирования пользователя.
(рис 9.4) Интерфейс обрезки изображений Gravatar с фоткой какого-то чувака.
Тесты для update действия похожи на аналогичные для create действия. Листинг 9.9 показывает как использовать Capybara для заполнения полей формы валидной информацией, а затем тестирует, что результирующее поведение корректно. Это большой кусок кода; посмотрим, сможете ли вы проработать его, опираясь на тесты из Главы 7.
require 'spec_helper'
describe "User pages" do
.
.
.
describe "edit" do
let(:user) { FactoryGirl.create(:user) }
before do
sign_in user
visit edit_user_path(user)
end
.
.
.
describe "with valid information" do
let(:new_name) { "New Name" }
let(:new_email) { "new@example.com" }
before do
fill_in "Name", with: new_name
fill_in "Email", with: new_email
fill_in "Password", with: user.password
fill_in "Confirm Password", with: user.password
click_button "Save changes"
end
it { should have_title(new_name) }
it { should have_selector('div.alert.alert-success') }
it { should have_link('Sign out', href: signout_path) }
specify { expect(user.reload.name).to eq new_name }
specify { expect(user.reload.email).to eq new_email }
end
end
end
Обратите внимание на то что Листинг 9.9 добавляет метод sign_in из Листинга 9.6 в блок before, что необходимо для прохождения тестов ссылки "Sign out" и также предполагает защиту действия edit от невошедших пользователей (Раздел 9.2.1).
Единственной новинкой в Листинг 9.9 является метод reload, который появляется в тесте для изменения атрибутов пользователя:
specify { expect(user.reload.name).to eq new_name }
specify { expect(user.reload.email).to eq new_email }
Этот код перезагружает user переменную из (тестовой) базы данных используя user.reload, а затем проверяет, что новые имя пользователя и email совпадают с новыми значениями.
Действие update, необходимое для прохождения тестов в Листинге 9.9 аналогично финальной форме create действия (Листин 8.27), как видно в Листинг 9.10. Единственное что он делает, это добавляет
flash[:success] = "Profile updated" redirect_to @user
к коду в Листинг 9.8.
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/
Одним из приятных эффектов построения механизма аутентификации в Главе 8 является то, что мы теперь готовы к реализации авторизации: аутентификация позволяет нам идентифицировать пользователей нашего сайта, а авторизация позволяет нам контролировать предоставляемые им возможности.
Хотя edit и update действия из Раздела 9.1 функционально завершены, они страдают от нелепой бреши в безопасности: они позволяют любым (даже незарегистрированным) пользователям иметь доступ к любому действию, и любой зарегистрированный пользователь может изменять информацию любого другого пользователя. В этом разделе мы реализуем модель безопасности, которая будет требовать от пользователей входа в систему и будет предотвращать обновление любой информации, кроме их собственной. Незарегистрированные пользователи, пытающиеся получить доступ к защищенным страницам будут перенаправлены на страницу входа с полезным сообщением, как это показано на рис 9.5.
(рис 9.5) Набросок результата посещения защищенной страницы
Поскольку ограничения безопасности для edit и update действий идентичны, мы будем обрабатывать их в одном RSpec describe блоке. Начав с требования входа, наши первоначальные тесты затем проверяют, что не вошедшие пользователи, пытающиеся получить доступ к какому либо из действий, просто перенаправляеются на страницу входа, как показано в Листинге 9.11.
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, лекция 7). Это необходимо из-за того, что браузер не может посетить непосредственно само действие 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.
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.)
Совместно с .
(рис 9.6) Форма входа после попытки получить доступ к защищенной странице.
В этой точке наш набор тестов должен решительно позеленеть:
$ bundle exec rspec spec/
Конечно, требования входа пользователей недостаточно; пользователи должны иметь доступ к редактированию только своей информации. Мы можем протестировать это, вначале войдя как неправильный пользователь, а затем обратившись к edit и update действиям (Листинг 9.13). Обратите внимание: поскольку мы не используем Capybara для этих тестов (no_capybara: true), мы используем методы get и patch для обращения к действиям edit и update напрямую. К тому же, так как пользователи никогда не должны даже пытаться изменить профиль другого пользователя, мы сделаем переадресацию не на страницу входа, а на корневой URL.
require 'spec_helper'
describe "Authentication" do
.
.
.
describe "authorization" do
.
.
.
describe "as wrong user" do
let(:user) { FactoryGirl.create(:user) }
let(:wrong_user) { FactoryGirl.create(:user, email: "wrong@example.com") }
before { sign_in user, no_capybara: true }
describe "submitting a GET request to the Users#edit action" do
before { get edit_user_path(wrong_user) }
specify { expect(response.body).not_to match(full_title('Edit user')) }
specify { expect(response).to redirect_to(root_url) }
end
describe "submitting a PATCH request to the Users#update action" do
before { patch user_path(wrong_user) }
specify { expect(response).to redirect_to(root_url) }
end
end
end
end
Обратите внимание на то, что фабрика может принимать опцию:
FactoryGirl.create(:user, email: "wrong@example.com")
Это создает пользователя с адресом электронной почты, отличающимся от дефолтного. Тесты описывают что этот пользователь не должен иметь доступа к действиям edit или update оригинального пользователя.
Код приложения добавляет второй предфильтр для вызова метода correct_user, как это показано в Листинге 9.14.
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).
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/
Защита наших страниц закончена, как и было написано, но есть один небольшой недостаток: когда пользователи пытаются получить доступ к защищенной странице, они перенаправляются к странице профиля, независимо от того, куда они пытались попасть. Другими словами, если не залогинившийся пользователь пытается посетить страницу редактирования, после входа пользователь будет перенаправлен на /users/1 вместо /users/1/edit. Было бы намного более доброжелательно, по отношению к пользователям, все же перенаправлять их на запрашиваемую страницу вместо этого .
Для того чтобы протестировать такую "дружелюбную переадресацию", мы вначале посещаем страницу редактирования пользователя, которая перенаправит нас на страницу входа. Затем мы введем валидную информацию для входа и кликнем по кнопке "Sign in". Результирующей страницей, которой по дефолту является профиль пользователя, в данном случае должна быть страница "Edit user". Тест для этой последовательности представлен в Листинге 9.16.
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
Теперь реализация.store_location и redirect_back_or, определенных в хелпере Sessions (Листинг 9.17).
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
В роли механизма хранения выступает объект
Для того чтобы использовать store_location, нам необходимо добавить ее в предфильтр signed_in_user, как это показано в Листинге 9.18.
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.)
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.7) Набросок списка пользователей с пагинацией и навигационной ссылкой “Users”.
Хотя мы сохраним страницы show отдельных пользователей видимыми для всех посетителей сайта, для страницы user index будет реализовано ограничение, отображающее ее только для зарегистрированных пользователей, также будет реализовано ограничение того, сколько зарегистрированных пользователей будет отображаться на каждой странице списка. Мы начнем с тестирования того, что действие index защищено, посетив users_path (Таблица 7.1, лекция 7) и проверив что мы перенаправлены на страницу входа. Как и с остальными тестами авторизации, мы поместим этот пример в интеграционные тест авторизации, как это показано в Листинге 9.20.
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.
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.
require 'spec_helper'
describe "User pages" do
subject { page }
describe "index" do
before do
sign_in FactoryGirl.create(:user)
FactoryGirl.create(:user, name: "Bob", email: "bob@example.com")
FactoryGirl.create(:user, name: "Ben", email: "ben@example.com")
visit users_path
end
it { should have_title('All users') }
it { should have_content('All users') }
it "should list each user" do
User.all.each do |user|
expect(page).to have_selector('li', text: user.name)
end
end
end
.
.
.
end
Как вы можете вспомнить из соответствующего действия в 'demo app' (Листинг 2.4), код приложения использует User.all для вытягивания всех пользователей из базы данных, присваивая их переменной экземпляра @users для использования в представлении, как это показано в Листинге 9.23. (Если отображение всех пользователей за раз кажется вам плохой идеей, вы правы и мы избавимся от этого недостатка в Разделе 9.3.3.)
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 прежде чем продолжать.
<% 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).
.
.
.
/* 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;
}
}
}
.
.
.
/* 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 лекции 7. Тест (Листинг 9.26) и код приложения (Листинг 9.27) довольно просты.
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
<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.8) Страница списка пользователей /users с одним пользователем.
В этом разделе мы дадим нашему одинокому образцу пользователя небольшую компанию. Конечно, чтобы создать достаточное количество пользователей, мы могли бы, использовать наш веб-браузер, для посещения страницы регистрации и создания новых пользователей по одному, но куда более продвинутым решением является использование Ruby (и Rake), чтобы сделать пользователей для нас.
Во-первых, мы добавим Faker гем в Gemfile, который позволит нам делать образцы пользователей с полу-реалистичными именами и адресами электронной почты (Листинг 9.28).
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. (Это довольно продвинутый материал, так что не особо заморачивайтесь деталями.)
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
end
end
Этот код определяет задачу db:populate которая создает образец пользователя с именем и адресом электронной почты, делая реплику нашего предыдущего пользователя, после чего делает еще 99 экземпляров. Строка
task populate: :environment do
обеспечивает Rake задаче доступ к локальному Rails окружению, включая модель User (и, следовательно, к User.create!). Здесь create! это метод очень похожий на create, за той лишь разницей, что он вызывает исключение (Раздел 6.1.4) при неудачном создании, вместо того чтобы тихо возвращать false. Эта крикливая конструкция упрощает отладку, помогая избежать тихих ошибок.
С пространством имен :db как в Листинге 9.29, мы можем вызвать Rake задачу следующим образом:
$ bundle exec rake db:reset $ bundle exec rake db:populate $ bundle exec rake test:prepare
После запуска Rake задачи, наше приложениее имеет 100 примеров пользователей, как это видно на рис 9.9. (Я взял на себя смелость связать первые несколько образцов адресов с фотографиями так что не все изображения являются дефолтными картинками Gravatar.)
(рис 9.9) Страница списка пользователей /users с 100 образцов пользователей.
Наш оригинальный пользователь более не страдает от одиночества, но теперь у нас появилась другая проблема: у нашего пользователя слишком большая компания, и вся она расположилась на одной странице. Сейчас это сотня, что уже является довольно большим числом, а на реальном сайте это могут быть и тысячи. Решение заключается в пагинации (разбиении на страницы, постраничном выводе) пользователей, так, чтобы (например) показывать только 30 на каждой странице одновременно.
В Rails есть несколько способов разбиения на страницы, мы будем использовать один из самых простых и надежных, он называется will_paginate. Для того, чтобы использовать его нам необходимо включить сам гем will_paginate, а также гем bootstrap-will_paginate, который конфигурирует will_paginate для использования пагинационных стилей предоставляемых Bootstrap. Обновленный Gemfile представлен в Листинге 9.30.
source 'https://rubygems.org' ruby '2.0.0' #ruby-gemset=railstutorial_rails_4_0 gem 'rails', '4.0.2' gem 'bootstrap-sass', '2.3.2.0' gem 'bcrypt-ruby', '3.1.2' gem 'faker', '1.1.2' gem 'will_paginate', '3.0.4' gem 'bootstrap-will_paginate', '0.0.9' . . .
Затем запустите bundle install:
Затем запустите bundle install:
Вам также следует перезагрузить веб-сервер для того чтобы убедиться, что новые гемы были загружены как следует.
Поскольку гем will_paginate широко распространен, нам нет необходимости тестировать его, так что мы можем применить несколько облегченный подход. Во-первых, мы протестируем наличие div с CSS классом "pagination", который выводится гемом will_paginate. Затем мы проверим что на первой странице результатов представлены правильные пользователи. Для этого нам потребуется использовать метод paginate, о котором мы вскоре узнаем подробнее.
Как и прежде, мы будем использовать Factory Girl для имитации пользователей, но мы тут же натыкаемся на проблему: адреса электронной почты пользователей должны быть уникальными, что, как представляется, требует создания более чем 30 пользователей вручную — ужасно муторная работа. К тому же, при тестировании выводимого списка пользователей было бы удобно, если бы у них были разные имена. К счастью, Factory Girl предвидела этот вопрос, и обеспечила последовательность (цикл) для ее решения. Наша оригинальная фабрика (Листинг 7.8) хард-кодила имя и адрес электронной почты:
FactoryGirl.define do
factory :user do
name "Michael Hartl"
email "michael@example.com"
password "foobar"
password_confirmation "foobar"
end
end
Вместо этого мы можем организовать последовательность имен и адресов электронной почты с помощью метода sequence:
factory :user do
sequence(:name) { |n| "Person #{n}" }
sequence(:email) { |n| "person_#{n}@example.com"}
.
.
.
Здесь sequence принимает символ соответствующий выбранному атрибуту (такому как :name) и блок с одной переменной, которую мы назвали n. При последующих вызовах FactoryGirl метод,
FactoryGirl.create(:user)
Переменная блока n автоматически увеличивается, таким образом, именем первого пользователя будет "Person 1", а адресом электронной почты - "person_1@example.com", второй пользователь получит имя "Person 2" и адрес электронной почты "person_2@example.com", и т.д.. Полный код представлен в Листинге 9.31.
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).
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); мы вскоре увидим почему код был добавлен сверху и снизу списка пользователей.
<% 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 сгенерировал автоматически.
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 сверху и снизу списка пользователей, ссылки на страницы появились в обоих местах.
(рис 9.10) Страница списка пользователей /users с пагинацией.
Если теперь кликнуть по любой 2 или Next ссылке, вы получите вторую страницу с результатами, как это показано на рис 9.11.
(рис 9.11) Страница 2 списка пользователей (/users?page=2).
Вам также следует проверить что тесты проходят:
$ bundle exec rspec spec/
Разбитый на страницы список пользователей теперь закончен, но есть одно улучшение, от которого я не могу удержаться: Rails имеет несколько невероятно ловких инструментов для создания компактных представлений, и в этом разделе мы займемся рефакторингом страницы со списком пользователей, используя эти инструменты. Так как наш код хорошо протестирован, мы можем с уверенностью приступить к рефакторингу, будучи уверенными в том, что мы вряд ли нарушим функциональность сайта.
Первым шагом нашего рефакторинга будет замена пользовательского li из Листинга 9.33 на вызов render (Листинг 9.35).
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
Здесь мы вызываем user непринципиально — мы могли бы написать @users.each do |foobar|, а затем использовать render foobar. Ключом является класс объекта — в данном случае, User._user.html.erb, который мы должны создать (Листинг 9.36).
<li> <%= gravatar_for user, size: 52 %> <%= link_to user.name, user %> </li>
Это явное улучшение, но мы можем сделать еще лучше: мы можем вызвать render непосредственно на переменную @users (Листинг 9.37).
<% 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/
Теперь, когда список пользователей завершен, осталось лишь одно каноничное REST действие: destroy. В этом разделе мы добавим ссылки для удаления пользователей, как это показано на 12" type="image" />, и определим destroy действие необходимое для выполнения удаления. Но мы начнем с создания класса уполномоченных на это административных пользователей.
(рис 9.12) Набросок списка пользователей с удаляющими ссылками.
Мы будем идентифицировать привилегированных пользователей с правами администратора посредством булевого атрибута admin в модели User, что, как мы увидим, автоматически приведет нас к методу admin? для проверки административного статуса. Мы можем написать тесты для этого атрибута, как в Листинге 9.38.
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.39" type="example">Листинг 9.39), приводя к модели данных показанной на class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, default: false
end
end
Обратите внимание на то, что мы добавили аргумент Наконец, мы мигрируем базу данных разработки и подготавливаем тестовую бд: Как и ожидалось, Rails догадывается о булевом характере атрибута В результате, тесты для admin должны пройти: В качестве последнего шага, давайте обновим наш заполнитель образцов данных для того, чтобы сделать первого пользователя администратором (Листинг 9.40). Наконец, перезапустим заполнитель для перезагрузки базы данных и последующей перестройки ее с нуля: Возвращение к строгим параметрам Вы могли заметить, что Листинг 9.40 делает пользователя администратором с помощью добавления Этот запрос сделал бы администратором пользователя 17, что могло бы стать потенциально серьезной брешью в безопасности, если не сказать больше. Из-за этой опасности очень важно передавать на дальнейшую обработку только параметры безопасных для редактирования атрибутов. Как отмечалось в Разделе 7.3.2, это может быть достигнуто с помощью строгих параметров вызывая В частности, обратите внимание на то, что Последний шаг, необходимый для завершения ресурса Users, заключается в добавлении удаляющих ссылок и Для написания тестов для удаляющего функционала нам пригодится фабрика создающая администраторов. Мы можем достигнуть этого, добавив блок С кодом в Листинге 9.41, мы теперь можем использовать Наша политика безопасности требует запретить обычным пользователям видеть удаляющие ссылки: Но административные пользователи должны видеть такие ссылки и мы ожидаем, что админ, кликнув по удаляющей ссылке удалит пользователя, т.e., изменит количество Это включает код Код приложения показывает удаляющие ссылки только если текущий пользователь является админом (Листинг 9.43). Обратите внимание на аргумент Изначально, веб браузеры не могут отправлять DELETE запросы, и Rails подделывает их с помощью JavaScript. Это означает, что удаляющие ссылки не будут работать если у пользователя отключен JavaScript. Если вы обязаны поддерживать браузеры с отключенным JavaScript, вы можете подделать запрос DELETE с помощью формы и запроса POST, что будет работать даже без JavaScript; более подробно об этом см. RailsCast о "Destroy Without JavaScript". Для того чтобы получить рабочие удаляющие ссылки, нам необходимо добавить действие Обратите внимание на то, что действие Даже несмотря на то, что только администраторы могут видеть ссылки на удаление, есть еще одна страшная дыра в безопасности: любой достаточно опытный злоумышленник может просто выдать запрос DELETE из командной строки и удалить любого пользователя на сайте. Для того, чтобы обеспечить безопасность сайта, мы также нуждаемся в контроле доступа, и наши тесты должны проверить не только то, что администраторы могут удалять пользователей, но также и то, что другие пользователи не могут этого делать. Результаты представлены в Листинге 9.45. Обратите внимание на то, что, по аналогии с методом В принципе, у нас осталась еще одна незначительная брешь в безопасности, которая заключается в том, что админ может удалить сам себя выдав запрос DELETE. Можно, конечно, сказать что такой админ Сам Себе Злой Буратино, но было бы неплохо предотвратить подобные случаи, что остается в качестве упражнения (Раздел 9.6). Как вы можете догадаться, реализация использует предфильтр, в этот раз для ограничения доступа к В этой точке все тесты должны проходить и ресурс Users — со своим контроллером, моделью и представлениями — функционально завершен. Мы прошли долгий путь с момента введения контроллера Users в Разделе 5.4. Те пользователи даже не могли зарегистрироваться; теперь же пользователи могут зарегистрироваться, войти в систему, выйти, просматривать свои профили, редактировать их параметры, и видеть список всех пользователей, а некоторые из них могут даже удалять других пользователей. Остальная часть этой книги будет опираться на ресурс Users (и связанную с ним аутентификационную систему) чтобы сделать сайт с Twitter-подобными микросообщениями (Глава 10) и потоком сообщений пользователей за которыми следит данный пользователь (Глава 11). Эти главы представят некоторые из наиболее мощных возможностей Rails, включая моделирование данных с Прежде чем двигаться дальше, убедитесь, что объединили все изменения в мастер ветку: Вы также можете задеплоить приложение и даже заполнить продакшен базу данных образцами пользователей (применив задачу Для того чтобы увидеть изменения вам возможно придется принудительно рестартовать приложение на Heroku: Стоит также отметить, что в этой главе мы в последний раз имели необходимость в установке гема. Для справки, окончательный вариант Gemfile показан в Листинге 9.47. (Необязательные гемы которые могут оказаться системозависимыми закомментированы. Вы можете раскомментировать их для того чтобы посмотреть работают ли они на вашей системе.)default: false к add_column в Листинге 9.39, что означает, что пользователи не будут администраторами по умолчанию. (Без аргумента default: false, admin был бы по умолчанию nil, что все же является false, так что этот шаг не является строго обязательным. Однако это более явно и четко сообщает о наших намерениях, и Rails, и читателям нашего кода.)
(рис 9.13) Модель User с добавленным булевым атрибутом admin.
$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
admin и автоматически добавляет метод со знаком вопроса admin?:$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true
$ bundle exec rspec spec/models/user_spec.rb
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
admin = User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true)
.
.
.
end
end
$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake test:prepare
patch /users/17?admin=1
require и permit на хэше params: def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
admin не включен в список разрешенных атрибутов. Именно это не позволяет произвольному пользователю предоставлять себе административный доступ к нашему приложению. Поскольку это очень важный нюанс, хорошей идеей будет написание теста на каждый нередактируемый атрибут и написание такого теста для атрибута admin оставлено в качестве упражнения (Раздел 9.6).Destroy действие
destroy действия. Мы начнем с добавления удаляющей ссылки для каждого пользователя на страницу со списком пользователей, при этом ограничив доступ к ним.:admin к нашим фабрикам как это показано в Листинг 9.41.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
FactoryGirl.create(:admin) для создания административных пользователей в наших тестах.it { should_not have_link('delete') }
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.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
method: :delete, который организует выдачу ссылками необходимого запроса DELETE. Мы также обернули каждую ссылку в if выражение, таким образом они видны только администраторам. Результат, видимый нашим административным пользователям, представлен на Рис. 9.14.<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>
(рис 9.14) Страница списка пользователей /users с удаляющими ссылками.
destroy(Таблица 7.1, лекция 7), которое будет находить соответствующего пользователя и удалять его с помощью метода Active Record destroy, по завершении перенаправляя пользователя на страницу списка пользователей, как это показано в Листинге 9.44. Обратите внимание, что мы также добавили :destroy в предфильтр signed_in_user.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
patch из Листинга 9.11, мы используем delete для непосредственной выдачи DELETE к указанному URL (в данном случае, путь к пользователю, как того требует Таблица 7.1, лекция 7).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
destroy действию всем пользователям кроме администраторов. Получившийся в результате предфильтр admin_user представлен в Листинге 9.46.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
$ bundle exec rspec spec/
Заключение
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 restart
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
Упражнения
update как это показано в Листинге 9.48, проверьте что атрибут admin не может быть отредактирован через веб. Вначале получите провальный тест и лишь потом добейтесь его прохождения. (Подсказка: Вашим первым шагом должно стать добавление admin в список разрешенных параметров в user_params.)_blank.sign_in из Листинга 9.6 в как можно большем количестве мест.new.html.erb и edit.html.erb представления с помощью партиала из Листинга 9.49. Обратите внимание на то, что вам придется явно передать пременную формы f в виде локальной переменной, как это показано в Листинге 9.50. Вам также понадобится обновить тесты, поскольку формы не останутся в точности такими же; найдите небольшую разницу между ними и обновите тесты соответствующим образом.new и create действиям контроллера Users. Организуйте для таких пользователей переадресацию в корневой URL, при попытке обратиться к этим страницам.destroy так, чтобы предотвратить уничтожение административными пользователями самих себя. (Начните с написания теста.)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
<%= 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 %>
<% 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></example>
<example number="9." title="" type="listing">
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
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.