StoneDot の Ruby on Rails 講座

Railsで簡単掲示板その2

今までとは趣向を変えて説明をがっつりと減らしてどんどんと進めていく方向で行きます。

今回は、前回作ったトピックモデルと、今回作るポストモデルを組み合わせる方法を説明していきます。

ポストモデルの作成

ここではさくっと投稿を表すポストモデルを作ってしまいたいと思います。

コマンドは前回と同じく rails generate scaffold を利用します。

rails generate scaffold Post contributor:text content:text post_number:integer

上記のコマンドで前回図で示したような情報を持つポストモデル等を生成することができます。

生成が完了したら、データベースに適用します。以下のコマンドでデータベースにマイグレーションを適用しておきましょう。

rake db:migrate

一通りの作業が終わったらサーバーを起動して、ちゃんと表示されるか確認しておきましょう。 サーバーの起動は「rails server」、URLは「http://localhost:3000/posts」です。

トピックとポストの関連付け

今回のアプリケーションでは必ずポストは何れかのトピックに関連づけられていました。

今回はそれを表す関連を作りたいと思います。まずは、データベースにポストテーブルからトピックテーブルへの参照を追加します。以下のコマンドで posts テーブルに topics テーブルへの参照用のカラムを追加するマイグレーションを作成することができます。

rails generate migration AddReferenceToTopicToPost topic_id:integer

Rails の規約により、「AddToTableA」の形になっている場合 TableA にカラムを追加するマイグレーションを作成してくれます。今回は To が二回出てきていますが、最後のほうの To の指定がちゃんと採用されていることがわかります。追加するカラムは scaffold の時と同様の方法で指定します。

今回は、トピックテーブルへの参照だったので topic_id:references として、参照を追加しています。Rails の規約により関連を記述する際に、参照先のテーブル名を単数形で書き、その後に _idを付与することになっているので、このような記述になっています。もし、 apples テーブルへの参照だった場合は apple_id:references と記述すればよいことになります。

さっそく「rake db:migrate」でマイグレーションを適用しておきましょう。

後は、Post モデルに topic テーブルに参照があることを教え、 Topic モデルに post を持つことを教えて上げれば完了です。

app/models/post.rbbelongs_to :topic を以下のように追加してください。

class Post < ActiveRecord::Base
  attr_accessible :content, :contributor, :post_nnumber
  belongs_to :topic
end

同様に、 app/models/topic.rbhas_many :posts を以下のように追加してください。

class Topic < ActiveRecord::Base
  attr_accessible :title
  has_many :posts
end

英語でも自然な文脈になるように、has_many の後に複数形の :posts が続いていることに注意してください。このように、 Rails のパラメータの指定は英語として読んだ時に自然になるように設計されています。

しかし、今回のように最初から関連が必要な事が分かっている場合は、 scaffold の時に以下のように指定したほうが良いでしょう。

rails generate scaffold Post contributor:text content:text post_number:integer \
topic:references

このようにすれば、テーブルへの topic_id:integer カラムの追加と、モデルへの belongs_to 文の追加を自動で行ってくれます。

Topic の show で投稿を表示

さて、今回のアプリケーションの構想では、トピック選択画面でトピックを選択するとそのトピックへの投稿を閲覧することが出来るようになっていました。これを実現しましょう。

posts オブジェクトのレンダリングは app/views/posts/index.html.erb に書かれてあるコードで行われています。これを拝借しましょう。

app/views/topics/show.html.erb に以下のように app/views/posts/index.html.erb の中身をコピーしてきます。

<p id="notice"><%= notice %></p>

<p>
  <b>Title:</b>
  <%= @topic.title %>
</p>

<table>
  <tr>
    <th>Contributor</th>
    <th>Content</th>
    <th>Post number</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @posts.each do |post| %>
  <tr>
    <td><%= post.contributor %></td>
    <td><%= post.content %></td>
    <td><%= post.post_number %></td>
    <td><%= link_to 'Show', post %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
</table>


<%= link_to 'Edit', edit_topic_path(@topic) %> |
<%= link_to 'Back', topics_path %>

これをそのまま実行できればうれしいのですが、あたり前ですがエラーになります。理由は @posts が空っぽだからです。これをコントローラー側で設定してあげましょう。

app/controllers/topics_controller.rbshow メソッドで以下のように @posts に中身を入れてあげましょう。

def show
  @topic = Topic.find(params[:id])
  @posts = @topic.posts

  respond_to do |format|
    format.html # show.html.erb
    format.json { render json: @topic }
  end
end

このように、コントローラで @posts といったインスタンス変数にオブジェクトを代入しておけば、レンダリングの際に使われるビューでも同じ名前 @posts でインスタンス変数を使えるようになります。

もしくは、ビューを編集して、 @posts となっているところを、 @topic.posts と書き換えるのもよいでしょう。ただし、これから先に出てくる app/views/topics/show.html.erb 内の @posts をすべて書き換える必要があります。

さて、実際に適当なトピックを表示してみましょう。今までの作業がうまくいっていればエラーは発生しないはずです。でも、残念ながら投稿は一件も表示されません。考えてみれば当たり前です。まだ、個々の投稿をトピックに関連付けてデータベースに保存していません。

トピックに関連付けて投稿する

現状ではポストを送信してもどのトピックにも関連付けられないため、トピックのページから見ても投稿を閲覧することができません。ここでは、トピックに関連付けてポストを送信出来るようにしましょう。

まずは、投稿ページを app/views/posts/_form.html.erb から、 app/views/topics/show.html.erb に移植しましょう。以下のような感じになります。

<p id="notice"><%= notice %></p>

<p>
  <b>Title:</b>
  <%= @topic.title %>
</p>

<table>
  <tr>
    <th>Contributor</th>
    <th>Content</th>
    <th>Post number</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @posts.each do |post| %>
  <tr>
    <td><%= post.contributor %></td>
    <td><%= post.content %></td>
    <td><%= post.post_number %></td>
    <td><%= link_to 'Show', post %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
</table>

<%= form_for(@post) do |f| %>
  <% if @post.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
      <% @post.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :contributor %><br />
    <%= f.text_area :contributor %>
  </div>
  <div class="field">
    <%= f.label :content %><br />
    <%= f.text_area :content %>
  </div>
  <div class="field">
    <%= f.label :post_number %><br />
    <%= f.number_field :post_number %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>


<%= link_to 'Edit', edit_topic_path(@topic) %> |
<%= link_to 'Back', topics_path %>

もちろん @post というインスタンス変数はないのでこのままではエラーとなります。

そこで、 app/controllers/posts_controller.rbnew メソッドを見ながら app/controllers/topics_controller.rbshow メソッドを編集します。

def show
  @topic = Topic.find(params[:id])
  @posts = @topic.posts
  @post = Post.new

  respond_to do |format|
    format.html # show.html.erb
    format.json { render json: @topic }
  end
end

ここでは、 @post = Post.new という行を追加しています。これでエラーが発生することは無くなりました。

次に行うのは、トピックの自動付与です。

そのために、 Post を Topic にルーティング的に関連付けると楽になります。 config/routes.rb で以下のようになっている部分があると思いますが、

resources :posts

resources :topics
これを以下のように書き換えます。

resources :topics do
  resources :posts
end

このようにしておくと form_for をちょっと書き換えるだけで、関連付けをすっきりと行うことができます。

以下のように、 app/views/topics/show.html.erbform_for を書き換えます。

<%= form_for([@topic, @post]) do |f| %>

このようにすると、 Create Post ボタンを押すことで /topics/{topic_id}/posts にリクエストが行くようになります。この形になっていれば、 topic_idposts_controller.rb 内で取得することが出来るようになります。

posts_controller.rb はデフォルトだと、 /posts以下のリクエストを処理するようになっているのですが、先ほど config/routes.rb を編集したため、 /topics/{topic_id}/posts 以下のリクエストを処理するように切り替えられたわけです。

これに合わせて、 app/controllers/posts_controller.rbcreate メソッドを編集します。

def create
  @topic = Topic.find(params[:topic_id])
  @post = @topic.posts.build(params[:post])

  respond_to do |format|
    if @post.save
      format.html { redirect_to [@topic, @post], notice: 'Post was successfully created.' }
      format.json { render json: @post, status: :created, location: [@post, @post] }
    else
      format.html { render action: "new" }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end

これで Topic に関連付けられた状態で Post が保存されるようになりました。

しかし、この状態だとありとあらゆるところで Routing Error が発生します。というのも、さきほどルーティングをいじったので、 /post/{post_id} だと Post オブジェクトにアクセス出来ないからです。

上記の app/controllers/posts_controller.rbcreate メソッドはこの問題を修正しています。

redirect_to @postredirect_to [@topic, @post] としているのが、その修正です。

これを参考にルーティングエラーをすべて修正するのは各自の宿題としましょう。

投稿番号の割り振り自動化

Post の要素として投稿番号 (Post number) というものがありますが、現状だとこれを手で入力しなければならない状態です。この投稿番号というのはトピック内で投稿を一意に識別できる1から始まる番号を意識したものです。ここでは、そのトピックで何番目の投稿なのかを判断して自動的に番号を振る機能の実装を行いたいと思います。

まずは、投稿の際の Post Number 入力欄を削除しましょう。 app/views/topics/show.html.erb を開いて、以下の部分を削除します。

<%= f.label :post_number %>
<%= f.number_field :post_number %>

以上で、入力欄の削除は完了です。後は、適当な方法で番号を振ればよいのですが、ここではトピックの中で最も大きい投稿番号に 1 を足したものをその投稿の投稿番号とすることにします。

実現のためには post のコントローラを書き換える必要があります。 app/controllers/posts_controllers.rbcreate メソッドを以下のように変更します。

def create
  @topic = Topic.find(params[:topic_id])
  @post = @topic.posts.build(params[:post])
  max_num = @topic.posts.maximum(:post_number)
  max_num = 0 if max_num.blank?
  @post.post_number = max_num + 1

  respond_to do |format|
    if @post.save
      format.html { redirect_to [@topic, @post], notice: 'Post was successfully created.' }
      format.json { render json: @post, status: :created, location: [@post, @post] }
    else
      format.html { render action: "new" }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end

まず、 @topic.posts で最大値の検索対象を現在のトピックの投稿に制限し、 .maximum(:post_nunmber) で実際に最大値を求めるカラムを指定し、求まった値を max_num に代入します。もしかしたら一件も投稿がないかもしれないので、 nil か空だったら 0 を max_num に代入しておきます。最終的には、そうやって求めた値に 1 を加えたものをデータベースに格納しているわけです。

Contributor の入力欄を一行にする

現在 Contributor の入力欄が複数行に渡るテキストエリアになっているのでこれを一行のテキストボックスにしてみましょう。

app/views/topics/show.html.erb を開き、

<%= f.text_area :contributor %>
となっているところを、
<%= f.text_field :contributor %>
のように変更します。

この <%= %> という表記は、そこにテキストを埋め込みたい場合に利用し、 <% %>という表記は、テキストを埋め込まない場合に使われることに注意してください。

今回は、テキストボックスの表示のためのHTMLの埋め込みだったので、前者を使用します。

text_field メソッドの引数は Post のどのカラムの値に対応するかを書きます。