Ruby On Railsでブログ機能(ログイン・閲覧・投稿・編集・削除・コメント)を実装をしたいと思います。

まずはRailsプロジェクトを作ります。

rails new sample

ログイン機能はDevise、デザインはTwitterBootStrapを用います。
※OAuth認証を提供するOmniauthというのもあるみたいですが今回は割愛します。
/Gemfile

# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'
 
  # See https://github.com/sstephenson/execjs#readme for more supported runtimes
  # gem 'therubyracer'
 
  gem 'uglifier', '>= 1.0.3'
  gem 'therubyracer'
  gem 'less-rails-bootstrap'
  gem 'twitter-bootstrap-rails'
  gem 'devise'
end

適用します。

bundle install

Deviseを使ってログイン画面・登録画面を作ってみます。

rails g devise:install

Userテーブルを作成します。ログイン機能です。

rails g devise user

Noteテーブルを作成します。CRUD機能です。

Note
-string:title
-text:memo

rails g scaffold note title:string memo:text --skip-stylesheets

間違えたときは

rails destroy scaffold モデル名の単数形

さて、ここまで来たので一旦確認
/config/routes.rb

Sample::Application.routes.draw do
  devise_for :users
  resources :notes
end

/app/models/user.rb

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  attr_accessible :email, :password, :password_confirmation, :remember_me
end

/app/models/note.rb

class Note < ActiveRecord::Base
  attr_accessible :memo, :title
end

/app/controllers/application_controller.rb
berfore_filterは追記して下さい。

class ApplicationController < ActionController::Base
  protect_from_forgery
  before_filter :authenticate_user!
end

DBを作ります。

rake db:migrate

Twitter BootStrapを適用します。

rails g bootstrap:install

基本となるデザインの修正
/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <title>Sample</title>
  <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script>
  <![endif]-->
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<!-- ナビゲーションバー -->
<div class="navbar navbar-fixed-top">
    <div class="navbar-inner">
        <div class="container">
            <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </a>
            <a class="brand" href="#">Some Sample</a>
            <div class="nav-collapse">
                <ul class="nav">
                    <li><%= link_to "Browse Products", notes_path %></li>
                    <li><%= link_to "Price List" %></li>
                    <li><%= link_to "Contact Us" %></li>
                    <li><%= link_to "Cart" %></li>
                </ul>
            </div>
        </div>
    </div>
</div>
<!-- ナビゲーションバー -->   
<!-- メインコンテンツ -->  
<div class="container">
    <div class="row">
        <div class="span9"><%= yield %></div>
        <div class="span3">
            <h2>About Us</h2>
            <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>        
        </div>
    </div>
</div>
<!-- メインコンテンツ -->       
</body>
</html>

基本となるCSSの修正
/app/assets/stylesheets/bootstrap_and_overrides.css

@import "twitter/bootstrap/bootstrap";
 
body { padding-top: 60px; } 
 
@import "twitter/bootstrap/responsive";
 
// Set the correct sprite paths
@iconSpritePath: asset-path('twitter/bootstrap/glyphicons-halflings.png');
@iconWhiteSpritePath: asset-path('twitter/bootstrap/glyphicons-halflings-white.png');
 
// Your custom LESS stylesheets goes here
//
// Since bootstrap was imported above you have access to its mixins which
// you may use and inherit here
//
// If you'd like to override bootstrap's own variables, you can do so here as well
// See http://twitter.github.com/bootstrap/less.html for their names and documentation
//
// Example:
// @linkColor: #ff0000;

最後に適用させます。

$ rails g bootstrap:themed notes -f
$ rails g bootstrap:themed users -f
$ rails s

確認
http://localhost:3000/notes

さて、userとnoteを関連付けます。

/app/models/user.rb
has_manyは追記して下さい。noteと関連付けています。

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  attr_accessible :email, :password, :password_confirmation, :remember_me
  has_many :notes
end

/app/models/note.rb
belongs_toとscopeは追記して下さい。userと関連付けています。

class Note < ActiveRecord::Base
  attr_accessible :memo, :title
  belongs_to :user
  scope :latest ,order('update_at desc')
end

さて、noteテーブルにuser_idを保持するカラムを追加します。notesのidとuser_idにindexを張ります。

rails g migration add_column_note

/db/migrate/20130615134444_add_column_note.rb

class AddColumnNote < ActiveRecord::Migration
  def up
    add_column :notes , :user_id , :integer 
    add_index :notes , :id
    add_index :notes , :user_id
  end

  def down
  end
end

反映内容を追記して、DBに反映させます。

rake db:migrate

NoteとUserを紐付けます。
Userの所有するNote以外は更新・削除出来ないようにします。
/app/controllers/notes_controller.rb


class NotesController < ApplicationController
  # GET /notes
  # GET /notes.json
  def index
#    全件取得
    @notes = Note.all
#    ログインユーザの情報を取得
    @user = current_user
#    @note = Note.where("user_id=?",@user.id).latest
    

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @notes }
    end
  end

  # GET /notes/1
  # GET /notes/1.json
  def show
#    全件表示
#    @note = Note.find(params[:id])
#    ログインユーザの情報表示
    @user = current_user
    @note = Note.find(params[:id])

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

  # GET /notes/new
  # GET /notes/new.json
  def new
#    全件
#    @note = Note.new
#    ログインユーザ
    @user = current_user
    @note = Note.new
    
    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @note }
    end
  end

  # GET /notes/1/edit
  def edit
    @note = Note.find(params[:id])
    @user = current_user
  end

  # POST /notes
  # POST /notes.json
  def create
    @note = Note.new(params[:note])
#   ログインユーザ
    @user = current_user
    @note.user_id = @user.id

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

  # PUT /notes/1
  # PUT /notes/1.json
  def update
    @user = current_user
    @note = Note.find(params[:id])
    check = 'PARAM_OK'

    if @note.user_id == @user.id
      check = 'PARAM_OK'
    else
      check = 'PARAM_NG'
    end

    case check
      when 'PARAM_OK'
        respond_to do |format|
            if @note.update_attributes(params[:note])
              format.html { redirect_to @note, notice: 'Note was successfully updated.' }
              format.json { head :no_content }
            else
              format.html { render action: "edit" }
              format.json { render json: @note.errors, status: :unprocessable_entity }
            end
        end
      when 'PARAM_NG'
        respond_to do |format|        
            format.html { redirect_to notes_url , notice: 'You can not edit this item' }
            format.json { head :no_content }
        end
      else
    end
  end

  # DELETE /notes/1
  # DELETE /notes/1.json
  def destroy
    @note = Note.find(params[:id])
    @user = current_user    
    check = 'PARAM_OK'
    
    if @note.user_id == @user.id
      check = 'PARAM_OK'
    else
      check = 'PARAM_NG'
    end
    
    case check
      when 'PARAM_OK'
        respond_to do |format|
            @note.destroy
            format.html { redirect_to notes_url }
            format.json { head :no_content }
        end
      when 'PARAM_NG'
        respond_to do |format|
            format.html { redirect_to notes_url , notice: 'You can not delete this item' }
            format.json { head :no_content }
        end
      else
    end
  end
  
end

ユーザの所有するコンテンツ以外はボタンを非表示にして削除・編集出来ないようにします。
/app/views/notes/index.html.erb


<%- model_class = Note -%>
<div class="page-header">
  <h1><%=t '.title', :default => model_class.model_name.human.pluralize %></h1>
</div>
<table class="table table-striped">
  <thead>
    <tr>
      <th><%= model_class.human_attribute_name(:id) %></th>
      <th><%= model_class.human_attribute_name(:title) %></th>
      <th><%= model_class.human_attribute_name(:memo) %></th>
      <th><%= model_class.human_attribute_name(:user_id) %></th>
      <th><%= model_class.human_attribute_name(:created_at) %></th>
      <th><%=t '.actions', :default => t("helpers.actions") %></th>
    </tr>
  </thead>
  <tbody>
    <% @notes.each do |note| %>
      <tr>
        <td><%= link_to note.id, note_path(note) %></td>
        <td><%= note.title %></td>
        <td><%= note.memo %></td>
        <td><%= note.user_id %></td>
        <td><%=l note.created_at %></td>
        <td>
          <% if note.user_id === @user.id %>            
          <%= link_to t('.edit', :default => t("helpers.links.edit")),
                      edit_note_path(note), :class => 'btn btn-mini' %>
          <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
                      note_path(note),
                      :method => :delete,
                      :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
                      :class => 'btn btn-mini btn-danger' %>
          <% end %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= link_to t('.new', :default => t("helpers.links.new")),
            new_note_path,
            :class => 'btn btn-primary' %>

個別ページもユーザの所有するコンテンツ以外はボタンを非表示にして削除・編集出来ないようします。
/app/views/notes/show.html.erb


<%- model_class = Note -%>
<div class="page-header">
  <h1><%=t '.title', :default => model_class.model_name.human %></h1>
</div>

<dl class="dl-horizontal">
  <dt><strong><%= model_class.human_attribute_name(:title) %>:</strong></dt>
  <dd><%= @note.title %></dd>
  <dt><strong><%= model_class.human_attribute_name(:memo) %>:</strong></dt>
  <dd><%= @note.memo %></dd>
  <dt><strong><%= model_class.human_attribute_name(:user_id) %>:</strong></dt>
  <dd><%= @note.user_id %></dd>
</dl>

<div class="form-actions">
  <%= link_to t('.back', :default => t("helpers.links.back")),
              notes_path, :class => 'btn'  %>
  <% if @note.user_id === @user.id %>            
  <%= link_to t('.edit', :default => t("helpers.links.edit")),
              edit_note_path(@note), :class => 'btn' %>
  <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
              note_path(@note),
              :method => 'delete',
              :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
              :class => 'btn btn-danger' %>
  <% end %>
</div>

確認してみましょう。

rails s

http://localhost:3000/notes

コメント機能を実装していきます。
memoと一対多で紐づくcommentを作っていきます。
#今回はSlideShareURLをコメントとして投稿する機能を作るのでslideshareという名前ですがcommentと置き換えて下さい。

rails g model slideshare

DBのERを作ります。memoに紐付けます。
/db/migrate/20130713051504_create_slideshares.rb

class CreateSlideshares < ActiveRecord::Migration
  def self.up
    create_table :slideshares do |t|
      t.column :note_id , :integer
      t.column :url , :string 
      t.timestamps
    end
  end
end

DBに反映します。

rake db:migrate

modelに親子関係をつけます。
/app/models/slideshare.rb

class Slideshare < ActiveRecord::Base
  attr_accessible :url
  belongs_to :note
end

:dependent => :delete_all としておくとmemoが削除された時、comment(slideshare)も削除されます。
accepts_nested_attributes_for :slideshares としておくと、noteの投稿時に複数のcomment(slideshare)を投稿出来ます。
attr_accessible :memo, :title , :slideshares_attributes としてActiveModel::MassAssignmentSecurity::Errorを防ぐ

/app/models/note.rb

class Note &lt; ActiveRecord::Base
  attr_accessible :memo, :title , :slideshares_attributes
  belongs_to :user
  scope :latest ,order(&#039;update_at desc&#039;)
  has_many :slideshares, :dependent => :delete_all
  accepts_nested_attributes_for :slideshares ,:reject_if => lambda { |a| a[:url].blank? }, :allow_destroy => true
end

/app/conrtollers/notes_controller.rb

  # POST /notes
  # POST /notes.json
  def create
    @note = Note.new(params[:note])
#   ログインユーザ
    @user = current_user
    @note.user_id = @user.id

    #SlideShareの紐付け投稿
    1.times do
    slideshare = @note.slideshares.build
    end
        
    respond_to do |format|
      if @note.save
        format.html { redirect_to @note, notice: &#039;Note was successfully created.&#039; }
        format.json { render json: @note, status: :created, location: @note }
      else
        format.html { render action: &quot;new&quot; }
        format.json { render json: @note.errors, status: :unprocessable_entity }
      end
    end
  end


  # GET /notes/new
  # GET /notes/new.json
  def new
#    全件
#    @note = Note.new
#    ログインユーザ
    @user = current_user
    @note = Note.new
    
    #SlideShareの紐付け投稿
    1.times do
    slideshare = @note.slideshares.build
    end
     
    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @note }
    end
  end


  # GET /notes/1
  # GET /notes/1.json
  def show
#    全件表示
#    @note = Note.find(params[:id])
#    ログインユーザの情報表示
    @user = current_user
    @note = Note.find(params[:id])
    slideshareId = @note.id
    @slideshare = Slideshare.find(:first , :conditions => { :note_id => slideshareId })
        
    #初期化
    @slideId = "NOT_GIVEN"
    if @slideshare.url.blank? 
    else
      @slideId = callSlideshare(@slideshare.url)      
    end

    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @note }
    end
  end
  
  def callSlideshare(slideurl)
    require 'rexml/document'
    require 'digest/sha1'
    require "net/https"
    require "uri"
    
    #SlideShare API呼び出し
    url_base = "https://www.slideshare.net/api/2/get_slideshow?slideshow_url="
    url_option  = "&exclude_tags=1&detailed=1&get_transcript=1"    
    api_key = "****"
    secret_key = "****"
    timestamp  = Time.now.to_i
    #SlideShre APIはsecret_key + timestampをSHA1でハッシュ化する必要がある
    hash = Digest::SHA1.hexdigest(secret_key+timestamp.to_s)
    #登録されたURLを正規表現に変更
    url_escape = CGI.escape(slideurl)
    #APIを叩くためのURL生成
    url = url_base + url_escape + "&api_key=" +  api_key + "&hash=" + hash + "&ts=" + timestamp.to_s + url_option
    uri = URI.parse(url)

    #APIリクエスト
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    request = Net::HTTP::Get.new(uri.request_uri)
    response = http.request(request)
    status = response.code
    
    case status
    when "200"
        #XML形式で取得
        doc = REXML::Document.new(response.body)   
        #必要項目を取得
        @slideId = doc.elements['Slideshow/ID'].text
      else
        @slideId = "NOT_GIVEN"        
    end
    
    return @slideId
  end


フォームをいじります。
/app/views/_form_html.erb

<%= form_for @note, :html => { :class => 'form-horizontal' } do |f| %>
  <div class="control-group">
    <%= f.label :title, :class => 'control-label' %>
    <div class="controls">
      <%= f.text_field :title, :class => 'text_field' %>
    </div>
  </div>
  <div class="control-group">
    <%= f.label :memo, :class => 'control-label' %>
    <div class="controls">
      <%= f.text_area :memo, :class => 'text_area' %>
    </div>
  </div>

  <div class="control-group">
  	    <div class="controls">
		  <%= f.fields_for :slideshares do |builder| %>
		    <%= render "slideshare_fields", :f => builder %>
		  <% end %>
		</div>
  </div>
    
  <div class="form-actions">
    <%= f.submit nil, :class => 'btn btn-primary' %>
    <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                notes_path, :class => 'btn' %>
  </div>
<% end %>

slideshareテーブル書き込み用フォーム
/app/views/_slideshare_fields.html.erb

<p>
  <%= f.label :url, "SlideShareURL" %><br />
  <%= f.text_field :url %><br />
  <%= f.check_box :_destroy %>
  <%= f.label :_destroy, "Remove" %>
</p>

現状、スライドは一件のみ表示しています。
/app/view/show_html.erb

<%- model_class = Note -%>
<div class="page-header">
  <h1><%=t '.title', :default => model_class.model_name.human %></h1>
</div>

<dl class="dl-horizontal">
  <dt><strong><%= model_class.human_attribute_name(:title) %>:</strong></dt>
  <dd><%= @note.title %></dd>
  <dt><strong><%= model_class.human_attribute_name(:memo) %>:</strong></dt>
  <dd><%= @note.memo %></dd>
  <% if @slideId != "NOT_GIVEN" %>  
  <dt><strong><%= model_class.human_attribute_name(:url) %>:</strong></dt>
  <dd><%= @slideshare.url %></dd>
  <dd>
  	<iframe src="http://www.slideshare.net/slideshow/embed_code/<%= @slideId %>?rel=0" width="427" height="356" 
  	          frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC;
  	          border-width:1px 1px 0;margin-bottom:5px" allowfullscreen webkitallowfullscreen mozallowfullscreen>
  	</iframe>
  </dd>
  <% end %>
  <!--dd><%== RDiscount.new(@note.memo).to_html %></dd-->
  <!--dd><%== RDiscount.new(@note.memo , :filter_html).to_html %></dd-->
  <dt><strong><%= model_class.human_attribute_name(:user_id) %>:</strong></dt>
  <dd><%= @user.email %></dd>
</dl>

<div class="form-actions">
  <%= link_to t('.back', :default => t("helpers.links.back")),
              notes_path, :class => 'btn'  %>
  <% if @note.user_id === @user.id %>            
  <%= link_to t('.edit', :default => t("helpers.links.edit")),
              edit_note_path(@note), :class => 'btn' %>
  <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
              note_path(@note),
              :method => 'delete',
              :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
              :class => 'btn btn-danger' %>
  <% else%>              
  <% end %>
</div>

複数件対応はイテレータ使って実装したいと思います。
どんどんコントローラが太ってきてますね。

続編
Ruby On Railsでブログ機能を実装 – RDiscountでMarkdownを導入する
http://asklife.info/archives/3656
Ruby On Rails URLを元にSlideShareAPIを使ってスライド資料を表示させる
http://asklife.info/archives/3678
Ruby On Railsで共通クラスを作成するディレクトリとPathの通し方
http://asklife.info/archives/3665

参考
Twitter BootStrapでScaffold
http://asklife.info/archives/3418

環境構築
http://asklife.info/archives/3589