Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails5「中級」チュートリアル(3-8)投稿機能: 新しい投稿を作成する(翻訳)

概要

概要

原著者の許諾を得て翻訳・公開いたします。

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。
翻訳と同時に動作をRails 5.1とRuby 2.5で検証しています。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(3-8)投稿機能: 新しい投稿を作成する(翻訳)

ここまでの投稿はseedで作った人工的なものでした。今度はユーザーが投稿できるようにユーザーインターフェイスを追加しましょう。

posts_controller.rbファイルにnewアクションとcreateアクションを追加します(Gist)。

# controllers/posts_controller.rb
...
  def new
    @branch = params[:branch]
    @categories = Category.where(branch: @branch)
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)
    if @post.save 
      redirect_to post_path(@post) 
    else
      redirect_to root_path
    end
  end
...

newアクションでは投稿を作成するフォームで用いるインスタンス変数をいくつか定義しています。@categoriesインスタンス変数の内部には特定のブランチのカテゴリが保存されます。@postインスタンス変数には、Railsフォームで必要となる新しい投稿のオブジェクトが保存されます。

createアクションでは、post_paramsメソッドでデータを持たせたPostオブジェクトを新規作成し、@postに保存します。このpost_paramsメソッドは次のようにprivateスコープ内で定義します(Gist)。

# controllers/posts_controller.rb
...
def post_params
  params.require(:post).permit(:content, :title, :category_id)
                       .merge(user_id: current_user.id)
end
...

この[permit](https://apidock.com/rails/ActionController/Parameters/permit)メソッドは、オブジェクトの属性をホワイトリスト化するのに使われます。これにより、指定の属性を渡すことを明示的に許可します。

PostsControllerの冒頭に次の行を追加します(Gist)。

# controllers/posts_controller.rb
...
before_action :redirect_if_not_signed_in, only: [:new]
...

このbefore_actionはRailsのフィルタです。サインインしていないユーザーが投稿を作成するページにアクセスできるようにしたくありません。そのために、newアクションが呼ばれる前にこのredirect_if_not_signed_inメソッドが呼び出されます。このメソッドは他のコントローラにも同様に必要になるので、application_controller.rbファイルにこのメソッドを定義しておきましょう。ついでにサインインしているユーザーをリダイレクトするメソッドもあれば今後便利なので、どちらも定義しておきましょう(Gist)。

# controllers/application_controller.rb
...
def redirect_if_not_signed_in
  redirect_to root_path if !user_signed_in?
end

def redirect_if_signed_in
  redirect_to root_path if user_signed_in?
end
...

ユーザーが投稿を作成するには、newのテンプレートが必要です。postディレクトリの下にnew.html.erbファイルを作成します(Gist)。

<!--- posts/new.html.erb -->
<div class="container new-post">
  <div class="row">
    <div class="col-sm-6 col-sm-offset-3">
      <h1>Create a new post</h1>
        <%= render 'posts/new/post_form' %>
    </div>
  </div>
</div>

newディレクトリを作成し、その下に_post_form.html.erbファイルを作成します(Gist)。

<!-- posts/new/_post_form.html.erb -->
<%= bootstrap_form_for(@post) do |f| %>
  <%= f.text_field  :title, 
                    maxlength: 100, 
                    placeholder: 'Title', 
                    class: 'form-control',
                    required: true, 
                    minlength: 5,
                    maxlength: 100 %>
  <%= f.hidden_field :branch, :value => @branch %>
  <%= f.text_area :content, 
                  rows: 6,
                  required: true, 
                  minlength: 20,
                  maxlength: 1000,
                  placeholder: 'Describe what you are looking for. E.g. specific interests, expertise level, etc.', 
                  class: 'form-control' %>
  <%= f.collection_select :category_id, @categories, :id, :name, class: 'form-control' %>
  <%= f.submit "Create a post", class: 'form-control' %>
<% end %>

このフォームはかなり素朴な作りです。フィールドの属性を定義し、collection_selectでカテゴリを1つ選択できるようにしています。

変更をcommitします。

git add -A
git commit -m "Create a UI to create new posts

- Inside the PostsController:
  define new and create actions
  define a post_params method
  define a before_action filter
- Inside the ApplicationController:
  define a redirect_if_not_signed_in method
  define a redirect_if_signed_in method
- Create a new template for posts"

フォームをテストするspecを書いてテストします。特定のリクエストを送信後に正しいレスポンスを得られることを確認するため、request specsから書くことにします。specディレクトリの下に以下のディレクトリを作成します。

spec/requests/posts

その下にnew_spec.rbファイルを作成します(Gist)。

# spec/requests/posts/new_spec.rb
require 'rails_helper'
include Warden::Test::Helpers
RSpec.describe "new", :type => :request do

  context 'non-signed in user' do
    it 'redirects to a root path' do
      get '/posts/new'
      expect(response).to redirect_to(root_path)
    end
  end

  context 'signed in user' do
    let(:user) { create(:user) }
    before(:each) { login_as user }

    it 'renders a new template' do
      get '/posts/new'
      expect(response).to render_template(:new)
    end
  end

end

前述したように、request specは結合テストの薄いラッパーを提供しているので、特定のリクエストが送信されたときに正しいレスポンスを取得できるかどうかをテストすることができます。include Warden::Test::Helpersの行は、テスト用のログインを行うlogin_asメソッドを使うために必要になります。

変更をcommitします。

git add -A
git commit -m "Add request specs for a new post template"

これまで作成したページをテストするrequest specを追加することもできます。

同じディレクトリにbranches_spec.rbファイルを作成します(Gist)。

# spec/requests/posts/branches_spec.rb
require 'rails_helper'
include Warden::Test::Helpers
RSpec.describe "branches", :type => :request do

  shared_examples 'render_templates' do
    it 'renders a hobby template' do
      get '/posts/hobby'
      expect(response).to render_template(:hobby)
    end

    it 'renders a study template' do
      get '/posts/study'
      expect(response).to render_template(:study)
    end

    it 'renders a team template' do
      get '/posts/team'
      expect(response).to render_template(:team)
    end
  end

  context 'non-signed in user' do
    it_behaves_like 'render_templates'
  end

  context 'signed in user' do
    let(:user) { create(:user) }
    before(:each) { login_as user }

    it_behaves_like 'render_templates'
  end

end

このようにして、すべてのブランチページのテンプレートがレンダリングできることをチェックします。同じコードを繰り返し避けるために[shared_examples](https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples)も使っています。

変更をcommitします。

git add -A
git commit -m "Add request specs for Posts branch pages' templates"

同様に、showテンプレートもレンダリングできることを確認します。同じディレクトリにshow_spec.rbファイルを作成します(Gist)。

# spec/requests/posts/show_spec.rb
require 'rails_helper'
include Warden::Test::Helpers
RSpec.describe "show", :type => :request do

  shared_examples 'render_show_template' do
    let(:post) { create(:post) }
    it 'renders a show template' do
      get post_path(post)
      expect(response).to render_template(:show)
    end
  end

  context 'non-signed in user' do
    it_behaves_like 'render_show_template'
  end

  context 'signed in user' do
    let(:user) { create(:user) }
    before(:each) { login_as user }

    it_behaves_like 'render_show_template'
  end

end

変更をcommitします。

git add -A
git commit -m "Add request specs for the Posts show template"

今度はユーザーが新しい投稿を作成できることを確かめるために、フォームをテストするfeature specを作成しましょう。features/postsディレクトリの下にcreate_new_post_spec.rbファイルを作成します(Gist)。

# spec/features/posts/create_new_post_spec.rb
require "rails_helper"

RSpec.feature "Create a new post", :type => :feature do
  let(:user) { create(:user) }
  before(:each) { sign_in user }

  shared_examples 'user creates a new post' do |branch|
    scenario 'successfully' do
      create(:category, name: 'category', branch: branch)
      visit send("#{branch}_posts_path")
      find('.new-post-button').click
      fill_in 'post[title]', with: 'a' * 20
      fill_in 'post[content]', with: 'a' * 20
      select 'category', from: 'post[category_id]' 
      click_on 'Create a post'
      expect(page).to have_selector('h3', text: 'a' * 20)
    end
  end

  include_examples 'user creates a new post', 'hobby'
  include_examples 'user creates a new post', 'study'
  include_examples 'user creates a new post', 'team'
end

変更をcommitします。

git add -A
git commit -m "Create a create_new_post_spec.rb file with feature specs"

newテンプレートに少し新しいデザインを適用しましょう。

以下のディレクトリに移動します。

assets/stylesheets/partials/posts

new.scssファイルを作成します(Gist)。

// assets/stylesheets/partials/posts/new.scss
.new-post {
  height: calc(100vh - 50px);
  background-color: white;
  h1 {
    text-align: center;
    margin: 25px 0;
  }
  input, textarea, select {
    width: 100%;
  }
}

ブラウザでこのテンプレートを開くと、以下のような基本フォームが表示されるはずです。

変更をcommitします。

git add -A
git commit -m "Add CSS to the Posts new.html.erb template"

最後に、すべてのフィールドに正しく入力されるようにしたいと思います。Postモデルにいくつかバリデーションを追加しましょう。Postモデルに以下のコードを追加します(Gist)。

# models/post.rb
...
validates :title, presence: true, length: { minimum: 5, maximum: 255 }
validates :content, presence: true, length: { minimum: 20, maximum: 1000 }
validates :category_id, presence: true
...

変更をcommitします。

git add -A
git commit -m "Add validations to the Post model"

バリデーションをspecでカバーしましょう。Postモデルのspecファイルを開きます。

spec/models/post_spec.rb

以下を追加します(Gist)。

# spec/models/post_spec.rb
context 'Validations' do
  let(:post) { build(:post) }

  it 'creates successfully' do 
    expect(post).to be_valid
  end

  it 'is not valid without a category' do 
    post.category_id = nil
    expect(post).not_to be_valid
  end

  it 'is not valid without a title' do 
    post.title = nil
    expect(post).not_to be_valid
  end

  it 'is not valid  without a user_id' do
    post.user_id = nil
    expect(post).not_to be_valid
  end

  it 'is not valid  with a title, shorter than 5 characters' do 
    post.title = 'a' * 4
    expect(post).not_to be_valid
  end

  it 'is not valid  with a title, longer than 255 characters' do 
    post.title = 'a' * 260
    expect(post).not_to be_valid
  end

  it 'is not valid without a content' do 
    post.content = nil
    expect(post).not_to be_valid
  end

  it 'is not valid  with a content, shorter than 20 characters' do 
    post.content = 'a' * 10
    expect(post).not_to be_valid
  end

  it 'is not valid  with a content, longer than 1000 characters' do 
    post.content = 'a' * 1050
    expect(post).not_to be_valid
  end
end  

変更をcommitします。

git add -A
git commit -m "Add specs for the Post model's validations"

specific_branchesブランチをmasterブランチにmergeします。

git checkout -b master
git merge specific_branches
git branch -D specific_branches

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。