Ruby on Rails Tutorial (14)

Chapter 9 Updating, showing, and deleting users 開始。user 云々はここで一旦終わりになるはず。Users resource の REST なアクションを云々とのこと。管理ユーザが云々みたいな話も出てくる模様。
とりあえず branch をナニ。

$ git checkout -b updating-users

9.1 Updating users

REST なので更新は PUT を使う模様。許可された (権限を持った) ユーザが、というあたりも含め、で実装していく模様。

9.1.1 Edit form

どんどん進めます。まず spec/requests/user_pages_spec.rb に以下を追加。

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

    describe "page" do
      it { should have_selector('h1',    text: "Update your profile") }
      it { should have_selector('title', text: "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

controller 方面 (app/controllers/users_controller.rb) にも手を入れます。

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

あるいは app/views/users/edit.html.erb を以下の内容で作成。

<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

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

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

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

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

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

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

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

試験実行してみれ、とありますね。

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

一応 green 終了を確認。とりあえず上の edit.html.erb は色々微妙なカンジなので WEBrick 起動してみてブラウザ出力なソレのソースを見てみました。

<div class="row">
  <div class="span6 offset3">
    <form accept-charset="UTF-8" action="/users/1" class="edit_user" 
     id="edit_user_1" method="post">
     <div style="margin:0;padding:0;display:inline">
      <input name="utf8" type="hidden" value="&#x2713;" />
      <input name="_method" type="hidden" value="put" />
      <input name="authenticity_token" type="hidden" 
       value="CRfHVnuowm8BNu6ZlOoSmzhM2XOhEAJo9Wf9wqTWlWA=" />
     </div>

      <label for="user_name">Name</label>
      <input id="user_name" name="user[name]" size="30" type="text" value="Rails Tutorial" />

hidden なナニが多いですね。つうか _method という名前が付いたソレは一体何かというと、ブラウザは PUT なリクエストが送れないので誤魔化すためのナニらしい。
これってどうやって判断してるのか、というと以下がその根拠らしい。

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

なーるほど。form_for を使って form を作ったら Rails は @user.new_record? が真なら POST を使うし偽であれば PUT を使う、とのこと。むむむむ。
さらに試験を追加とのこと。
spec/requests/authentication_pages_spec.rb

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

      it { should have_selector('title', text: 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) }

Settings の行が追加ですね。あと test helper に sign_in というソレを追加。

def sign_in(user)
  visit signin_path
  fill_in "Email",    with: user.email
  fill_in "Password", with: user.password
  click_button "Sign in"
  # Sign in when not using Capybara as well.
  cookies[:remember_token] = user.remember_token
end

あるいは app/views/layouts/_header.html.erb を以下に、とのこと。

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", root_path, id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home", root_path %></li>
          <li><%= link_to "Help", help_path %></li>
          <% if signed_in? %>
            <li><%= link_to "Users", '#' %></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>

おそらくは Settings なリンクのソレが云々なはず。WEBrick 再起動して動作確認しました。

9.1.2 Unsuccessful edits

詳細略で users_controller.rb に update なアクション追加。

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

で、試験実行しとけ、とのこと。ええと試験 green 終了なのか。

9.1.3 Successful edits

を、成功時になりましたね。試験も valid なソレが追加とのこと。
spec/requests/user_pages_spec.rb

    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_selector('title', text: new_name) }
      it { should have_selector('div.alert.alert-success') }
      it { should have_link('Sign out', href: signout_path) }
      specify { user.reload.name.should  == new_name }
      specify { user.reload.email.should == new_email }
    end

試験見ると確かに update してるっぽいですね。で、controller も以下に修正せよ、とのこと。
app/controllers/users_controller.rb

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(params[:user])
      flash[:success] = "Profile updated"
      sign_in @user
      redirect_to @user

で、試験実行なのか。なんとなく最近 red 確認して、ってソレが略されてる気がしますがスルーします。試験 green 終了確認。

9.2 Authorization

承認とか認証とかアレ。

9.2.1 Requiring signed-in users

とりあえず spec/requests/authentication_pages_spec.rb に試験を追加。

  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_selector('title', text: 'Sign in') }
        end

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

signin していないユーザはリダイレクトされてますね。つうかいきなり put しちゃう試験もあったりなんかしてますね。
controller 側では before_filter というソレを使って対応します。アクションメソド実行前に呼び出される、はず。

class UsersController < ApplicationController
  before_filter :signed_in_user, only: [:edit, :update]
  .
  .
  .
  private

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

edit および update の場合、signed_in? が偽ならリダイレクトしてますね。ちなみに signed_in_user 手続きは

  • flash[:notide] = "Please sign in."
  • redirect_to signin_url

と同じことみたい。あと、spec/requests/user_pages_spec.rb を先んじて以下に修正しておきます。

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

これで試験実行ってなってますがどうなるんだろ。At this point, test suite should be green. とある通り、green 終了しました。むむむ。

9.2.2 Requiring the right user

とりあえず as wrong user な試験から着手なのかな。他のユーザを云々な試験が、なのかどうか。以下な試験が app/requests/authentication_pages_spec.rb 方面に追加とあります。

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

      describe "visiting Users#edit page" do
        before { visit edit_user_path(wrong_user) }
        it { should_not have_selector('title', text: full_title('Edit user')) }
      end

      describe "submitting a PUT request to the Users#update action" do
        before { put user_path(wrong_user) }
        specify { response.should redirect_to(root_path) }
      end
    end

成程。試験の可読性が良いってのはアレですね。で、app/controllers/users_controller.rb 方面にいくつか盛り込みが必要らしい。

  • before_filter に correct_user 追加
  • edit アクションは空に
  • update アクションも find 略で
  • private な correct_user メソド追加

before_filter で find してるので、ってことみたいですね。面白い。Listing 9.15 を以下に引用しておきます。
app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_filter :signed_in_user, only: [:edit, :update]
  before_filter :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end

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

    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_path) unless current_user?(@user)
    end
end

盛り込んだ。あと、app/helpers/sessions_helper.rb に以下な current_user? を盛り込み。

  def current_user?(user)
    user == current_user
  end

で試験実行してみるに green 終了を確認。今日はちょっとここで終わります。週末対応で全部は終わりそうにないですね。