Разработка приложений на Ruby on Rails

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

Разбить на страницы
Показывать лекцию целиком

В этой главе мы завершим 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. Изображение взято на http://www.flickr.com/photos/sashawolff/4598355045/. Как обычно, мы начнем с тестов. Во-первых, обратите внимание на ссылку для смены изображения Gravatar; если вы зайдете на сайт Gravatar, вы увидите, что страница для добавления или редактирования изображений находится по адресу http://gravatar.com/emails, и мы протестируем наличие на странице edit этого URL. Сайт Gravatar на самом деле переадресует на http://en.gravatar.com/emails, предназначенную для англоговорящих пользователей, но я опустил en часть для возможного использования других языков.

(рис 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 запроса и скрытого поля Не беспокойтесь о том, как это работает, детали представляют интерес для разработчиков самого фреймворка Rails, но, по замыслу, не имеют значения для разработчиков Rails приложений.

Стоит также упомянуть здесь еще одну тонкость: код 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

Теперь реализация.Код в этом разделе это адаптация (переделка) гема Clearance команды разработчиков thoughtbot Для того чтобы перенаправить пользователей к запрашиваемой ими странице, нам нужно где-то сохранить запрашиваемую страницу, а затем переадресовать к ней. Мы добьемся этого с помощью пары методов, 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/

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

В этом разделе мы добавим предпоследнее действие пользователя, действие .Фотография ребенка из http://www.flickr.com/photos/glasgows/338937124/ В Разделе 9.4 мы добавим административный интерфейс к списку пользователей для того чтобы (предположительно проблемные) могли быть удалены.

(рис 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. в этом контексте, Rails автоматически ищет партиал с названием _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

Обратите внимание на то, что мы добавили аргумент 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

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

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

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

$ bundle exec rspec spec/models/user_spec.rb

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

namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    admin = User.create!(name: "Example User",
                         email: "example@railstutorial.org",
                         password: "foobar",
                         password_confirmation: "foobar",
                         admin: true)
    .
    .
    .
  end
end

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

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

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

Вы могли заметить, что Листинг 9.40 делает пользователя администратором с помощью добавления Инструменты командной строки, такие как curl могут выдавать PATCH запросы такой формы.

patch /users/17?admin=1

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

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

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

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

Destroy действие

Последний шаг, необходимый для завершения ресурса 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	

Упражнения

  • Направив запрос PATCH напрямую к методу update как это показано в Листинге 9.48, проверьте что атрибут admin не может быть отредактирован через веб. Вначале получите провальный тест и лишь потом добейтесь его прохождения. (Подсказка: Вашим первым шагом должно стать добавление admin в список разрешенных параметров в user_params.)
  • Организуйте открытие Gravatar ссылки "change" из Листинга 9.3 в новом окне (или вкладке). Подсказка: Ищите в сети; вы должны найти один простой и надежный метод с участием так называмого _blank.
  • Текущий тест аутентификации проверяет что навигационные ссылки, такие как "Profile" и "Settings" появляются когда пользователь входит. Добавьте тест проверяющий что эти ссылки не видны невошедшим пользователям.
  • Примените тестовый хелпер sign_in из Листинга 9.6 в как можно большем количестве мест.
  • Удалите дублирующийся код формы отрефакторив new.html.erb и edit.html.erb представления с помощью партиала из Листинга 9.49. Обратите внимание на то, что вам придется явно передать пременную формы f в виде локальной переменной, как это показано в Листинге 9.50. Вам также понадобится обновить тесты, поскольку формы не останутся в точности такими же; найдите небольшую разницу между ними и обновите тесты соответствующим образом.
  • Зарегистрированным пользователям совершенно незачем иметь доступ к new и create действиям контроллера Users. Организуйте для таких пользователей переадресацию в корневой URL, при попытке обратиться к этим страницам.
  • Изучите объект http://api.rubyonrails.org/v4.0.0/classes/ActionDispatch/Request.html в шаблон сайта. (Обращайтесь к Листингу 7.1, если застрянете.)
  • Напишите тест для проверки того, что дружелюбная переадресация направляет к данному URL только в первый раз. На последующие попытки входа, адрес перенаправления должени меняться на дефолтный (т.e., страницу профиля). См. подсказку в Листинге 9.51 (и подсказкой в данном случае я называю решение).
  • Модифицируйте действие 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. Изображение взято на http://www.flickr.com/photos/sashawolff/4598355045/. Как обычно, мы начнем с тестов. Во-первых, обратите внимание на ссылку для смены изображения Gravatar; если вы зайдете на сайт Gravatar, вы увидите, что страница для добавления или редактирования изображений находится по адресу http://gravatar.com/emails, и мы протестируем наличие на странице edit этого URL. Сайт Gravatar на самом деле переадресует на http://en.gravatar.com/emails, предназначенную для англоговорящих пользователей, но я опустил en часть для возможного использования других языков.

    (рис 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 запроса и скрытого поля Не беспокойтесь о том, как это работает, детали представляют интерес для разработчиков самого фреймворка Rails, но, по замыслу, не имеют значения для разработчиков Rails приложений.

    Стоит также упомянуть здесь еще одну тонкость: код 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

    Теперь реализация.Код в этом разделе это адаптация (переделка) гема Clearance команды разработчиков thoughtbot Для того чтобы перенаправить пользователей к запрашиваемой ими странице, нам нужно где-то сохранить запрашиваемую страницу, а затем переадресовать к ней. Мы добьемся этого с помощью пары методов, 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/

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

    В этом разделе мы добавим предпоследнее действие пользователя, действие .Фотография ребенка из http://www.flickr.com/photos/glasgows/338937124/ В Разделе 9.4 мы добавим административный интерфейс к списку пользователей для того чтобы (предположительно проблемные) могли быть удалены.

    (рис 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. в этом контексте, Rails автоматически ищет партиал с названием _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

    Обратите внимание на то, что мы добавили аргумент 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

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

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

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

    $ bundle exec rspec spec/models/user_spec.rb

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

    namespace :db do
      desc "Fill database with sample data"
      task populate: :environment do
        admin = User.create!(name: "Example User",
                             email: "example@railstutorial.org",
                             password: "foobar",
                             password_confirmation: "foobar",
                             admin: true)
        .
        .
        .
      end
    end

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

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

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

    Вы могли заметить, что Листинг 9.40 делает пользователя администратором с помощью добавления Инструменты командной строки, такие как curl могут выдавать PATCH запросы такой формы.

    patch /users/17?admin=1

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

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

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

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

    Destroy действие

    Последний шаг, необходимый для завершения ресурса 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	

    Упражнения

  • Направив запрос PATCH напрямую к методу update как это показано в Листинге 9.48, проверьте что атрибут admin не может быть отредактирован через веб. Вначале получите провальный тест и лишь потом добейтесь его прохождения. (Подсказка: Вашим первым шагом должно стать добавление admin в список разрешенных параметров в user_params.)
  • Организуйте открытие Gravatar ссылки "change" из Листинга 9.3 в новом окне (или вкладке). Подсказка: Ищите в сети; вы должны найти один простой и надежный метод с участием так называмого _blank.
  • Текущий тест аутентификации проверяет что навигационные ссылки, такие как "Profile" и "Settings" появляются когда пользователь входит. Добавьте тест проверяющий что эти ссылки не видны невошедшим пользователям.
  • Примените тестовый хелпер sign_in из Листинга 9.6 в как можно большем количестве мест.
  • Удалите дублирующийся код формы отрефакторив new.html.erb и edit.html.erb представления с помощью партиала из Листинга 9.49. Обратите внимание на то, что вам придется явно передать пременную формы f в виде локальной переменной, как это показано в Листинге 9.50. Вам также понадобится обновить тесты, поскольку формы не останутся в точности такими же; найдите небольшую разницу между ними и обновите тесты соответствующим образом.
  • Зарегистрированным пользователям совершенно незачем иметь доступ к new и create действиям контроллера Users. Организуйте для таких пользователей переадресацию в корневой URL, при попытке обратиться к этим страницам.
  • Изучите объект http://api.rubyonrails.org/v4.0.0/classes/ActionDispatch/Request.html в шаблон сайта. (Обращайтесь к Листингу 7.1, если застрянете.)
  • Напишите тест для проверки того, что дружелюбная переадресация направляет к данному URL только в первый раз. На последующие попытки входа, адрес перенаправления должени меняться на дефолтный (т.e., страницу профиля). См. подсказку в Листинге 9.51 (и подсказкой в данном случае я называю решение).
  • Модифицируйте действие 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
    Вернуться к учебному плану