Railsチュートリアル 4.0版の演習について、自分なりの解答をまとめてみた

Railsチュートリアル4.0版には各章に演習が付いていますが、模範解答みたいなものは示されていないため、この書き方で良いのかと悩むこともありました。
そのため、復習した時のことを考えて、自分の解答をまとめておきます*1
なお、4章はsample_appと関係無いため、6章はコードが全て示されているため、それらの章の解答は省略します。

第3章

演習3.5.2はコードが示されているため、省略します。

演習3.5.1

spec/requests/static_pages_spec.rb *2

describe "Static pages" do
  .
  .
  .
  describe "Contact page" do
    it "should have the right title" do
      visit '/static_pages/contact'
      expect(page).to have_title("Ruby on Rails Tutorial Sample App | Contact")
    end

    it "should have the content 'Contact'" do
      visit '/static_pages/contact'
      expect(page).to have_content('Contact')
    end
  end
end

第5章

演習5.6.3はコードが示されているため、省略します。

演習5.6.1

spec/requests/static_pages_spec.rb

describe "Static pages" do
  .
  .
  .
  describe "Help page" do
    before { visit help_path }
    let(:heading)    { 'Help' }
    let(:page_title) { 'Help' }

    it_should_behave_like "all static pages"
  end


  describe "About page" do
    before { visit about_path }
    let(:heading)    { 'About' }
    let(:page_title) { 'About Us' }

    it_should_behave_like "all static pages"
  end


  describe "Contact page" do
    before { visit contact_path }
    let(:heading)    { 'Contact' }
    let(:page_title) { 'Contact' }

    it_should_behave_like "all static pages"
  end
end

演習5.6.2

spec/requests/static_pages_spec.rb

describe "Static pages" do
  .
  .
  .
  it "should have the right links on the layout" do
    visit root_path
    click_link "About"
    expect(page).to have_title(full_title('About Us'))

    click_link "Help"
    expect(page).to have_title(full_title('Help'))

    click_link "Contact"
    expect(page).to have_title(full_title('Contact'))

    click_link "Home"
    click_link "Sign up now!"
    expect(page).to have_title(full_title('Sign up'))

    click_link "sample app"
    expect(page).to have_title(full_title(''))
  end
  .
  .
  .
end

第7章

演習7.6.1, 7.6.3, 7.6.4 はコードが示されているため、省略します。

演習7.6.2

spec/requests/user_pages_spec.rb

describe "User pages" do
  .
  .
  .
  describe "signup" do
    .
    .
    .
    describe "after submission" do
      before { click_button submit }
      it { should have_title('Sign up') }
      it { should have_content('error') }
      it { should have_content("Password can't be blank") }
      it { should have_content("Password is too short (minimum is 6 characters) ") }
      it { should have_content("Name can't be blank ") }
      it { should have_content("Email can't be blank ") }
      it { should have_content("Email is invalid ") }
    end
    .
    .
    .

第8章

演習8.5.1

以下の2つを修正しました。

class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:email].downcase)
    if user && user.authenticate(params[:password])
      sign_in user
      redirect_to user
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end
<% provide(:title, "Sign in") %>
  .
  .
  .
  <div class="span6 offset3">
    <%= form_tag(sessions_path) do %>
      <%= label_tag :email %>
      <%= text_field_tag :email %>

      <%= label_tag :password %>
      <%= password_field_tag :password %>

      <%= submit_tag("Sign in", class: "btn btn-large btn-primary") %>
    <% end %>
    .
    .
    .

演習8.5.2

今見返したところ、課外活動はやっていなかったため、課外活動以外の内容を記載しておきます。


spec/support/utilities.rb のように、「RSpec::Matchers.define :have_welcome_message」というカスタムマッチャーを追加しました。

.
.
.
RSpec::Matchers.define :have_welcome_message do |message|
  match do |page|
    expect(page).to have_selector('div.alert.alert-success', text: message)
  end
end


カスタムマッチャーは、spec/requests/user_pages_spec.rb のように使いました。

-        it { should have_selector('div.alert.alert-success', text: 'Welcome') }
+        it { should have_welcome_message('Welcome') }

第9章

課題9.6.6, 9.6.7, 9.6.8はコードが示されているため、省略します。

課題9.6.1

app/controllers/users_controller.rbのuser_params に 「:admin」を追加することで、最初にRedとなりました。



課題9.6.2

app/views/users/edit.html.erb

-    <a href="http://gravatar.com/emails">change</a>
+    <%= link_to "change", "http://gravatar.com/emails", target: ["_blank"] %>

課題9.6.3

ユーザーがサインインしていない時というのを、以下のように考えて解答しました。

  • 最初にサインインページへ遷移した時
  • サインインに失敗した時


spec/requests/authentication_pages_spec.rb

describe "Authentication" do
  .
  .
  .
  describe "signin page" do
    .
    .
    .
    it { should_not have_link('Profile') }
    it { should_not have_link('Settings') }
  end

  describe "signin" do
    .
    .
    .
    describe "with invalid information" do
      .
      .
      .
      it { should_not have_link('Profile') }
      it { should_not have_link('Settings') }
      .
      .
      .

課題9.6.4

なるべく多くと書いてありますが、見つけられたのは spec/requests/authentication_pages_spec.rbのこの部分 のみでした。

-          fill_in "Email",    with: user.email
-          fill_in "Password", with: user.password
-          click_button "Sign in"
+          sign_in user

課題9.6.5

テストの更新は、spec/support/utilities.rb のvalid_signupメソッド に対して行いました。

def valid_signup
  .
  .
  .
  #fill_in "Confirmation", with: "foobar"
  fill_in "Confirm Password", with: "foobar"
end

課題9.6.9

最初に、 spec/requests/authentication_pages_spec.rb へとテストコードを追加しました。

describe "Authentication" do
  .
  .
  .
  describe "authorizaiton" do
    .
    .
    .
    describe "as an admin user" do
      let(:admin) { FactoryGirl.create(:admin) }
      before do
        sign_in admin, no_capybara: true
      end

      describe "submitting a DELETE request to the Users#destroy action" do
        before { delete user_path(admin) }
        specify { expect(response).to redirect_to(root_path) }
      end
    end
  end
end


次に、rails_projects/sample_app/の改良として、管理者が自分自身以外を削除できるよう、app/controllers/users_controller.rb のように、destroyメソッドへsessions_helper#current_user? を使った条件分岐を追加しました。

  .
  .
  .
  def destroy
    user = User.find(params[:id])
    if current_user? user
      redirect_to(root_path)
    else
      user.destroy
      flash[:success] = "User destroyed."
      redirect_to users_url
    end
  end
  .
  .
  .

第10章

課題10.5.7のチャレンジは行っていないため、省略します。

課題10.5.1

spec/requests/micropost_pages_spec.rb

describe "Micropost pages" do
  .
  .
  .
  describe "micropost creation" do
    .
    .
    .
    describe "with valid information" do
      before { fill_in 'micropost_content', with: "Lorem ipsum" }
      it "should create a micropost" do
        expect { click_button "Post" }.to change(Micropost, :count).by(1)
      end

      describe "and post 1 micropost" do
        before { click_button "Post" }
      
        it { should have_content('1 micropost') }
        it { should_not have_content('1 microposts') }
      end

      describe "and post 2 microposts" do
        before do
          click_button "Post"
          fill_in "micropost_content", with: "hoge"
          click_button "Post"
        end

        it { should have_content('2 microposts') }
      end
    end
    .
    .
    .

課題10.5.2

リスト9.33を参考に、spec/requests/micropost_pages_spec.rb のように修正しました。

describe "Micropost pages" do
  .
  .
  .
  describe "pagination" do
    before do
     40.times { FactoryGirl.create(:micropost, user: user) }
     visit root_path
    end
    after { Micropost.delete_all }

    it { should have_selector('div.pagination') }

    it "should list each micropost" do
      user.microposts.paginate(page: 1).each do |micropost|
        expect(page).to have_selector('li', text: micropost.content)
      end
    end
  end
  .
  .
  .

課題10.5.3

app/views/static_pages/home.html.erbの<% if signed_in? %>〜<% end %>をパーシャルに分けました。

<% if signed_in? %>
  <%= render 'shared/home_signed_in' %>
<% else %>
  <%= render 'shared/home_no_signed_in' %>
<% end %>
<div class="row">
  <aside class="span4">
    <section>
      <%= render 'shared/user_info' %>
    </section>
    <section>
      <%= render 'shared/micropost_form' %>
    </section>
  </aside>
  <div class="span8">
    <h3>Micropost Feed</h3>
    <%= render 'shared/feed' %>
  </div>
</div>
<div class="center hero-unit">
  <h1>Welcome to the Sample App</h1>
  <h2>
      This is the home page for the
    <a href="http://railstutorial.jp">Ruby on Rails Tutorial</a>
      sample application
  </h2>
  <%= link_to "Sign up now!", signup_path, class: "btn btn-large btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails"), 'http://rubyonrails.org/' %>

課題10.5.4

spec/requests/micropost_pages_spec.rb

describe "Micropost pages" do
  .
  .
  .
  describe "other user's micropost without delete_link" do
    let(:other_user) { FactoryGirl.create(:user) }
    before do
      FactoryGirl.create(:micropost, user: other_user)
      visit user_path(other_user)
    end

    it { should_not have_link('delete') }
  end
end

課題10.5.5

app/views/shared/_micropost_delete_link.html.erb のように、パーシャルを作成しました。

<% if current_user?(object.user) %>
  <%= link_to "delete", object, method: "delete",
                                   data: { confirm: "You sure?" },
                                   title: object.content %>

<% end %>


パーシャルを呼び出す側としては、app/views/shared/_feed_item.html.erb のように、パーシャルへ渡すオブジェクトとして、「feed_item」を指定しました。

    </span>
-   <% if current_user?(feed_item.user) %>
-     <%= link_to "delete", feed_item, method: "delete",
-                                      data: { confirm: "You sure?" },
-                                      title: feed_item.content %>
-
-   <% end %>
- </li>
+   <%= render 'shared/micropost_delete_link', object: feed_item %>
+ </li>

もう一方は、app/views/microposts/_micropost.html.erb のように、パーシャルへ渡すオブジェクトとして、「micropost」を指定しました。

    </span>
-   <% if current_user?(micropost.user) %>
-     <%= link_to "delete", micropost, method: "delete",
-                                      data: { confirm: "You sure?" },
-                                      title: micropost.content %>
-   <% end %>
- </li>
+   <%= render 'shared/micropost_delete_link', object: micropost %>
+ </li>

課題10.5.6

リスト10.47のwrapヘルパーを作成後(app/helpers/microposts_helper.rb)、app/views/microposts/_micropost.html.erb のように修正しました。

<li>
-  <span class="content"><%= micropost.content %></span>
+  <span class="content"><%= wrap(micropost.content) %></span>
  .
  .
  .

第11章

演習11.5.2はコードが示されているため、省略します。

演習11.5.1

spec/models/user_spec.rb

describe User do
  .
  .
  .
  describe "relationship associations" do
    let(:other_user) { FactoryGirl.create(:user) }
    before do
      @user.save
    end
    
    it "should destroy followed user" do
      @user.follow!(other_user)
      relationships = @user.relationships.to_a
      other_user.destroy
      expect(relationships).not_to be_empty
      relationships.each do |relationship|
        expect(Relationship.where(id: relationship.id)).to be_empty
      end
    end

    it "should destroy follower user" do
      @user.follow!(other_user)
      relationships = @user.relationships.to_a
      @user.destroy
      expect(relationships).not_to be_empty
      relationships.each do |relationship|
        expect(Relationship.where(id: relationship.id)).to be_empty
      end
    end
  end
end

演習11.5.3

「具体的には〜」の部分から、対象のviewは以下のとおりと考えました。

  • app/views/users/show.html.erb
  • app/views/users/show_follow.html.erb
  • app/views/shared/_user_info.html.erb


それらで共通して使用している部分は以下と考え、その部分をパーシャル化することにしました(app/views/shared/_user_gravatar.html.erb)。


また、パーシャル内では userという変数を使っているため、呼ぶときには明示的に

<%= render 'shared/user_gravatar', user: current_user %>

として渡すようにしました。

-      <h1>
-        <%= gravatar_for @user, size: 40 %>
-        <%= @user.name %>
-      </h1>
+      <%= render 'shared/user_gravatar', user: @user %>
-      <%= gravatar_for @user %>
-      <h1><%= @user.name %></h1>
+      <%= render 'shared/user_gravatar', user: @user %>
-<a href="<% user_path(current_user) %>">
-  <%= gravatar_for current_user, size: 52 %>
-</a>
-<h1>
-  <%= current_user.name %>
-</h1>
+<%= render 'shared/user_gravatar', user: current_user %>
<a href="<% user_path(user) %>">
  <%= gravatar_for user, size: 52 %>
</a>
<h1>
  <%= user.name %>
</h1>

演習11.5.4

Homeページのモデルに従ってとのことなので、プロファイルページも同じようなコードでテストを書くことにしました。
プロファイルページは、 users/[id] (UserController#show) と考え、spec/requests/user_pages_spec.rb のように修正しました。

describe "User pages" do
  .
  .
  .
  describe "profile page" do
    .
    .
    .
    describe "following/follower statistics" do
      let(:other_user) { FactoryGirl.create(:user) }
      before do
        other_user.follow!(user)
        visit user_path(user)
      end

      it { should have_link("0 following", href: following_user_path(user)) }
      it { should have_link("1 followers", href: followers_user_path(user)) }
    end
  end
  .
  .
  .

*1:GitHubへのリンクも付け加えたけれど、演習ごとにコミットしていなかったので、コミットを見るだけでは分かりづらい...

*2:今見たら、should have the content 'About Us' になっていた...