StoneDot の Ruby on Rails 講座

Railsで簡単掲示板その3

今回は前回までに作った掲示板ののブラッシュアップを行っていきたいと思います。

モデルの検証(バリデーション)

掲示板でいろいろと遊んでいると気づけるのですが、トピックを作成する際にタイトルを空欄のままにすることができます。また、以前と同じタイトルのトピックを作成することができます。

やはり、タイトルからどのトピックなのかすぐに分かるようにするためにも、タイトルには重複がない方が良いですし、タイトルが空というのはもってのほかです。

そういうわけで、トピックのタイトルに以上のような制約条件をつけたいと思います。

Rails にはデータが制約条件を満たしているのかを確認するための機構として、バリデーションというものがあります。ここでは、このバリデーションを使って重複の検知などを行ってみましょう。

バリデーションはモデルクラスに書き込んでいきます。 app/models/topic.rb を開いて以下のように中身を書き換えましょう。

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

  validates_presence_of :title
  validates_uniqueness_of :title
end

以上のような記述 validates_presence_of で要素が空でないこと、 validates_uniqueness_of で要素が一意であることを検証することができます。

実際に重複したタイトルをつけられないことや、タイトルを空にできないことを確認しておきましょう。

ただし、完全に同時に同じタイトルのトピックを作ると、同じタイトルのトピックを作成できる可能性があります。このような可能性を完全に排除したい場合はデータベースレベルで制約を書くようにする必要があります。

ポストにバリデーションの追加

トピックのタイトルと同様にポストの方にもバリデーションを書いておきましょう。

ポストの制約としては投稿者 (contributor) が空でないこと、 内容 (content) が空でないことぐらいで十分でしょうか?

これを実現するために app/models/post.rb を以下のように編集すれば良いでしょう。

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

  validates_presence_of :contributor
  validates_presence_of :content
end

これで、うまく動くことを確認しておいてください。

投稿失敗時の処理の改良

現在、投稿失敗時に表示される URL を確認すると、 /topics/{id}/posts となっていることが確認できます。しかし実際にレンダリングされている画面は /topics/{id}/posts/new のようです。確かにこれでも良いかもしれませんが、投稿時は /topics/{id} の画面を表示しているので、失敗時にもこちらの画面を表示してくれる方が親切な気がします。そこで、これを達成するための改良を行いたいと思います。

現状の app/controllers/posts_controller.rb のコードをのぞいてみると

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
の部分から投稿失敗時に posts の new のテンプレートを使ってレンダリングを行っていることが分かります。

そこで以下のようにコードを書きかえます。

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
      @topic = Topic.find(params[:topic_id])
      @posts = @topic.posts
      format.html { render "topics/show" }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end

ここで、@topic を代入し直していることに気をつけてください。

上で使われている @topic を失敗時のレンダリングにそのまま利用しようとするとルーティングエラーが発生します。これは @topic.posts.build を利用しているのが原因です。

いらないリンクを削除する

現状では個々の post の表示ができるようになっていますが、この機能はいらないですし、編集機能も使わない気がします。そこで、これらのリンクはすべて削除してしまいましょう。

以下のように app/views/topics/show.html.erb を編集します。折角のテコ入れなのでテーブルの使用をやめるなどの変更も行いました。

<h1><%= @topic.title %></h1>

<div id="post-list">
<% @posts.each do |post| %>
  <div class="post">
    <div class="post-header">
      <%= post.post_number %>:<%= post.contributor %></div>
    </div>
    <div class="post-body">
      <%= post.content %>
    </div>
    <div class="post-footer">
      <%= link_to '削除', [@topic, post], method: :delete, data: { confirm: '本当によろしいですか?' } %>
    </div>
  </div>
<% end %>
</div>

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

<%= form_for([@topic, @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_field :contributor, :size => 20 %>
  </div>
  <div class="field">
    <%= f.label :content, '投稿内容' %><br />
    <%= f.text_area :content, :size => '60x5' %>
  </div>
  <div class="actions">
    <%= f.submit '投稿' %>
  </div>
<% end %>


<%= link_to 'トピック一覧に戻る', topics_path %>

Post のいらないアクションを削除する

いろいろと改良を重ねていくうちに使わなくても良いようなビュー、アクションが増えてきました。そこで、いらないものを消す作業を行いたいと思います。

いらないと思われるのは、 app/views/posts 以下のビューすべてと、 app/controllers/posts_controller.rbindex, show, new, edit, update メソッドでしょうか。

以上のファイル(ディレクトリ)やメソッドを削除してしまいましょう。 posts_controller.rb は以下のようにだいぶシンプルになるはずです。

class PostsController < ApplicationController
  # POST /posts
  # POST /posts.json
  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
        @topic = Topic.find(params[:topic_id])
        @posts = @topic.posts
        format.html { render "topics/show" }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /posts/1
  # DELETE /posts/1.json
  def destroy
    @topic = Topic.find(params[:topic_id])
    @post = Post.find(params[:id])
    @post.destroy

    respond_to do |format|
      format.html { redirect_to topic_posts_url(@topic) }
      format.json { head :no_content }
    end
  end
end

次に、消したメソッドに合わせてルーティングのほうも編集しておきましょう。

以下のように config/routes.rb の resources 部分を編集してください。

resources :topics do
  resources :posts, :only => [:create, :destroy]
end

これで、 posts_controllercreatedestroy だけにルーティングされるようになりました。

このままだと、posts_controller での処理後に存在しないアクションに飛ばされてしまうので、 app/controllers/posts_controllers.rb を編集して、 topics/{id} にリダイレクトすることにしましょう。

class PostsController < ApplicationController
  # POST /posts
  # POST /posts.json
  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, notice: '投稿されました。' }
        format.json { render json: @post, status: :created, location: @topic }
      else
        @topic = Topic.find(params[:topic_id])
        @posts = @topic.posts
        format.html { render "topics/show" }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /posts/1
  # DELETE /posts/1.json
  def destroy
    @topic = Topic.find(params[:topic_id])
    @post = Post.find(params[:id])
    @post.destroy

    respond_to do |format|
      format.html { redirect_to @topic }
      format.json { head :no_content }
    end
  end
end

以上のようにすれば、期待する場所にリダイレクトされます。ブラウザで動作を確認しておきましょう。

トピック一覧画面の整理

トピック一覧画面を編集して綺麗にまとめておきましょう。 以下のように app/views/topics/index.html.erb を書き換えましょう。

<h1>トピック一覧</h1>

<ul>
<% @topics.each do |topic| %>
  <li>
    <%= link_to topic.title, topic %>:
    <%= link_to '削除', topic, method: :delete, data: { confirm: '本当によろしですか?' } %>
  </li>
<% end %>
</ul>

<br />

<%= link_to '新しいトピック', new_topic_path %>
	

この編集によって edit, update アクションがいらなくなったので、それに関する編集を行います。

まず、 app/views/topics/edit.html.erb を削除します。次に topics_controller.rb から edit, update アクションを削除します。

最後に、config/routes.rb を以下のように書き換えれば edit アクションに関する設定は完了です。

resources :topics, :except => [:edit, :update] do
  resources :posts, :only => [:create, :destroy]
end

まとめ

これで掲示板作成は完了です。お疲れ様でした。

まだまだ直したいというところもあると思うので、そこは各自取り組んでもらえればと思います。

特にやることが思いつかないなぁという方は、トピックの作成時に最初の投稿も一緒に行うように改良を加えてもらえれば面白いのかなと思っていますが、ちょっと難しいでしょうか?

もちろん、見た目にこだわって CSS を使い始めるのも良いかと思います。

Rails でのスタイルシートの埋め込み方はいろいろなページで言及しているのでそちらを参考にやってみるとよいと思います。

ここから一工夫して自分独自の味付けをしてもらう、それが今回の宿題です。