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.rb
に belongs_to :topic
を以下のように追加してください。
class Post < ActiveRecord::Base
attr_accessible :content, :contributor, :post_nnumber
belongs_to :topic
end
同様に、 app/models/topic.rb
に has_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.rb
の show
メソッドで以下のように @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.rb
の new
メソッドを見ながら app/controllers/topics_controller.rb
の show
メソッドを編集します。
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.erb
の form_for
を書き換えます。
<%= form_for([@topic, @post]) do |f| %>
このようにすると、 Create Post ボタンを押すことで /topics/{topic_id}/posts
にリクエストが行くようになります。この形になっていれば、 topic_id
を posts_controller.rb
内で取得することが出来るようになります。
posts_controller.rb
はデフォルトだと、 /posts
以下のリクエストを処理するようになっているのですが、先ほど config/routes.rb
を編集したため、 /topics/{topic_id}/posts
以下のリクエストを処理するように切り替えられたわけです。
これに合わせて、 app/controllers/posts_controller.rb
の create
メソッドを編集します。
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.rb
の create
メソッドはこの問題を修正しています。
redirect_to @post
を redirect_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.rb
の create
メソッドを以下のように変更します。
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 のどのカラムの値に対応するかを書きます。