坊やがゆく

2007-02-20

Railsでソーシャルブックマークを作ってみようか(第2回)

説明

Railsアプリを作る「はじめの一歩」としての足がかりになればと思いまとめました。

手順に沿ってコピペしていくといつのまにかアプリケーションが完成するというサンプルです。

第1回masuidriveさんベースRails勉強会@東京第11回での高橋征義さんバージョンとInternet Week 2006でのかずひこさんバージョンをミックスしました。

環境やインストール、趣旨や概要につきましては第1回をご覧ください。


■第1回との相違点

■今回実装を見送ったもの
  • 各種機能のブックマークレット用意
  • ローカライズ(ActiveHeartを使用する予定)
  • ユニットテスト、機能テスト
  • URIのルーティング(はてなのようにURIにユーザ名を含める形式にする)
  • デバッグ(夢のステップ実行)
  • security_extensionsの適用(secure_form_tagではなくform_tagを使用した)
  • Ajaxの実装(かなり未定)

今回使用したRailsのバージョン

rails -v

Rails 1.2.2

■バージョンを更新する場合

次のコマンドで最新バージョンに更新します。

gem update rails

※バージョンにはお気を付け下さい。私はバージョンが古いという原因で動作しないサンプルと何日も格闘してしまいました。。。


はてなブックマークっぽいソーシャルブックマークを作ってみよう

はてなブックマークっぽいソーシャルブックマークってどんなの?

こんな感じです。

ここまではムリですが、それっぽいものを作ります。


モデリング

ソーシャルブックマークとして最低限持つべき情報は次のようになります

  • ○○さんが
  • ○○というページに
  • ○○というコメントでブックマークする

これをUser,Page,Bookmarkというモデルに割り当てます

User
ログインID,パスワード
Page
URI,タイトル
Bookmark
ユーザID,ページID,コメント,作成日時

※各モデルにはPrimary Keyとして'id'が存在します


■プロジェクトを始める

rails bookmark


自動で生成されるディレクトリは次のようになります(主なものを抜粋)

app/
アプリケーションの本体
app/models/
モデル用のrbファイル
app/controllers/
コントローラ用のrbファイル
app/views/
ビュー用のrhtmlファイル
app/helpers/
ビューヘルパー用のrbファイル
config/
データベースの接続パラメータなどの設定ファイル
db/
データベースの構造を記述したファイル
public/
HTTPサーバのルートディレクトリ
test/
テストのためのファイル
vendor/
プラグインなど

文字コードの設定

config/database.ymlを編集(データベース文字コード

development:
  adapter: mysql
  database: bookmark_development
  username: root
  password: ****
  host: localhost
  encoding: utf8

config/enviroment.rbを編集(ruby文字コード

$KCODE = 'u'

※先頭に追加


app/controllers/application.rbを編集(Webサーバのcharset)

  before_filter :set_charset
  protected
  def set_charset
    @headers["Content-Type"] = "text/html; charset=utf-8"
  end

データベースの作成

mysql -u -root -p

create database bookmark_development default character set utf8;

MySQL Command Line Clientを使用すると楽です


モデル作成

■pageモデルの作成

ruby script/generate model page


db/migrate/001_create_pages.rbを編集

  def self.up
    create_table :pages do |t|
      t.column :uri, :string, :limit=>1024
      t.column :title, :string, :limit=>1024
    end
  end

■acts_as_authenticatedプラグインインストール

ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/acts_as_authenticated


■userモデル/accountコントローラの作成

ruby script/generate authenticated user account


■bookmarkモデルの作成

ruby script/generate model bookmark


db/migrate/003_create_bookmarks.rbを編集

  def self.up
    create_table :bookmarks do |t|
      t.column :user_id, :integer, :null => false
      t.column :page_id, :integer, :null => false
      t.column :comment, :string, :limit =>1024
      t.column :created_at, :datetime
    end
  end

データベースに反映

rake migrate


■モデル間のリレーションを定義

app/models/page.rbを編集(Page<1>----<多>Bookmark<多>----<1>User)

class Page < ActiveRecord::Base
  has_many :users, :through => :bookmarks
  has_many :bookmarks, :order => "created_at desc"

app/models/user.rbを編集(User<1>----<多>Bookmark<多>----<1>Page)

class User < ActiveRecord::Base
  has_many :pages, :through => :bookmarks
  has_many :bookmarks, :order => "created_at desc"

app/models/bookmark.rbを編集(UserPageを参照, 同一user_id中のpage_idはユニーク)

class Bookmark < ActiveRecord::Base
  belongs_to :user
  belongs_to :page

  validates_uniqueness_of :page_id, :scope => :user_id

モデル間のリレーションは次のように参照する

あるユーザがブックマークしている全ページ
a_user.pages
あるユーザの全ブックマーク
a_user.bookmarks
あるページをブックマークしている全ユーザ
a_page.users
あるページの全ブックマーク
a_page.bookmarks
あるブックマークのユーザ
a_bookmark.user
あるブックマークのページ
a_bookmark.page

■バリデーションを定義

app/models/page.rbを編集

  private

  def validate
    begin
      parsed_uri = URI.parse(uri)
      raise unless parsed_uri.host
      raise unless %w(http https).include?(parsed_uri.scheme)
    rescue
      errors.add(:uri, "invalid URI")
    end
  end

■アクセサのオーバライドを定義(titleカラムが空の場合にURIを返す)

app/models/page.rbを編集

  def title
    self[:title].blank? ? self[:uri] : self[:title]
  end

※self.titleを使用すると無限ループするので注意!


コントローラ作成

■pageコントローラの作成

ruby script/generate controller page


app/controllers/page_controller.rbを編集(ページのブックマーク一覧の表示)

class PageController < ApplicationController
  include AuthenticatedSystem
  before_filter :login_required

  def show
    @myuser = current_user
    @page = Page.find_by_uri(params[:uri])
  end
end

■userコントローラの作成

ruby script/generate controller user


app/controllers/user_controller.rbを編集(ユーザのブックマーク一覧の表示)

class UserController < ApplicationController
  include AuthenticatedSystem
  before_filter :login_required

  def show
    @myuser = current_user
    @user = params[:id].nil? ? @myuser : User.find(params[:id])
  end
end

■bookmarkコントローラの作成

ruby script/generate controller bookmark


app/controllers/bookmark_controller.rbを編集(ブックマークの追加)

class BookmarkController < ApplicationController
  include AuthenticatedSystem
  before_filter :login_required

  def add
    @myuser = current_user
    @page = Page.find_by_uri(params[:uri]) || Page.new(:uri => params[:uri])
    @page.title = params[:title]
    @bookmark = Bookmark.new
    @bookmark.user = @myuser
    @bookmark.page = @page
    @bookmark.comment = params[:comment]
    if request.post? && (@bookmark.save! rescue false)
      redirect_to :controller => "user", :action => "show", :id => @myuser
    else
      render(:action => "add")
    end
  end
end

ビュー作成

■pageビューを編集

app/views/page/show.rhtmlを作成

<h1><%= link_to(h(@page.title), h(@page.uri)) %></h1>
<ul>
<% for bookmark in @page.bookmarks -%>
  <li>
    <%= bookmark.created_at.strftime("%Y年%m月%d日") %>
    <%= link_to(h(bookmark.user.login), :controller => "user", :action => "show", :id => bookmark.user) %>
    <%= h(bookmark.comment) %>
  </li>
<% end -%>
</ul>

■userビューを編集

app/views/user/show.rhtmlを作成

<h1><%= h(@user.login) %>さんのブックマーク</h1>
<ul>
<% for bookmark in @user.bookmarks.find(:all, :include => [:page]) -%>
  <li>
    <%= bookmark.created_at.strftime("%Y年%m月%d日") %>
    <%= link_to(h(bookmark.page.title), :controller => "page", :action => "show", :uri => bookmark.page.uri) %>
    <%= h(bookmark.comment) %>
  </li>
<% end -%>
</ul>

■bookmarkビューを編集

app/views/bookmark/add.rhtmlを作成

<h1>ブックマークの追加</h1>

<% if request.post? -%>
<%= error_messages_for "page" %>
<%= error_messages_for "bookmark" %>
<% end -%>

<%= form_tag %>
<dl>
  <dt>URI</dt>
  <dd><%= text_field_tag "uri", @page.uri, :size => 40 %></dd>
  <dt>タイトル</dt>
  <dd><%= text_field_tag "title", @page.title, :size => 40 %></dd>
  <dt>コメント</dt>
  <dd><%= text_field_tag "comment", @bookmark.comment, :size => 40 %></dd>
</dl>
<p><%= submit_tag %></p>
<%= end_form_tag %>

動かしてみよう

WEBrickの起動

ruby script/server


■サインアップ

http://localhost:3000/account/signup/


ログイン

http://localhost:3000/account/login/


■ログアウト

http://localhost:3000/account/logout/


ブックマークの追加

http://localhost:3000/bookmark/add/


■ユーザのブックマーク一覧の表示

http://localhost:3000/user/show/

または、ページのブックマーク一覧からユーザをクリックすると表示します。


■ページのブックマーク一覧の表示

ユーザのブックマーク一覧からページをクリックすると表示します。


完成!

おつかれさまでした。


はてなブックマークっぽい見た目にする

さて、機能は一通り完成しましたが見た目が寂しいですね。

そこではてなブックマークのデザインを拝借してそれっぽくしてみましょう。


■pageビューを編集

public/stylesheets/page.cssを作成

body {
  background:#FFFFFF none repeat scroll 0%;
  color:#000000;
  margin:0px;
  padding:0px;
}

#bannersub {
  background:#5279E7 none repeat scroll 0%;
  border-bottom:1px solid #06289B;
  color:#C9D5F8;
}
#bannersub table {
  width:100%;
}
#bannersub td {
  color:#C9D5F8;
  font-size:80%;
  text-align:center;
}
#bannersub td a {
  color:#C9D5F8;
  text-decoration:none;
}
#bannersub td a.username {
  text-decoration:underline;
}

#container {
  margin-left:10px;
  margin-right:10px;
  margin-top:20px;
}

#body, div.body {
  margin-left:4%;
  margin-right:4%;
  z-index:2;
}

h2 {
  font-size:100%;
  margin-bottom:8px;
  padding-bottom:5px;
  padding-left:25px;
}
h2.entry-title {
  background:#5279E7;
  color:#FFFFFF;
  margin:15px 0pt 0pt;
  padding:0px;
}
h2.entry-title span {
  display:block;
  padding:7px 5px 5px 9px;
}

.entry-curve-bottom {
  clear:both;
  display:block;
  font-size:1px;
  height:8px;
  padding-bottom:10px;
}

.caption {
  font-size:90%;
}

.bookmarklist ul {
  background-color:#EDF1FD;
  border-top:1px solid #5279E7;
  font-size:90%;
  line-height:150%;
  list-style-position:inside;
  list-style-type:circle;
  margin:0px;
  padding:5px;
}
.bookmarklist ul {
  font-size:90%;
  line-height:150%;
  list-style-position:inside;
  list-style-type:circle;
}
span.timestamp {
font-size:90%;
}

app/views/layouts/page.rhtmlを作成

<head>
  <title>Page: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'page' %>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield  %>
</body>

app/views/page/show.rhtmlを編集

<div id="bannersub">
  <table cellspacing="0">
    <tbody>
      <tr>
        <td width="80%" nowrap="nowrap" style="text-align: left;"> ようこそ<%= link_to(h(@myuser.login), :controller => "user", :action => "show") %>さん </td>
        <td nowrap="nowrap"><%= link_to(h("ログアウト"), :controller => "account", :action => "logout") %></td>
      </tr>
    </tbody>
  </table>
</div>
<div id="container">
  <div id="body">
    <h2 class="entry-title"><span><%= link_to(h(@page.title), h(@page.uri)) %></span></h2>
    <span style="margin-bottom: 15px;" class="entry-curve-bottom"></span>
    <div class="bookmarklist">
      <div class="caption"><a id="comments" name="comments">このエントリーをブックマークしているユーザー</a> (<span title="パブリックモードのブックマーク数" class="public-count"><%= @page.bookmarks.count %></span>) </div>
      <ul>
      <% for bookmark in @page.bookmarks -%>
        <li id="bookmark-user">
          <span class="timestamp"><%= bookmark.created_at.strftime("%Y年%m月%d日") %></span>
          <%= link_to(h(bookmark.user.login), :controller => "user", :action => "show", :id => bookmark.user) %>
          <span class="comment"><%= h(bookmark.comment) %></span>
        </li>
      <% end -%>
      </ul>
    </div>
  </div>
</div>

■userビューを編集

public/stylesheets/user.cssを作成

body {
  background:#FFFFFF none repeat scroll 0%;
  color:#000000;
  margin:0px;
  padding:0px;
}

#bannersub {
  background:#5279E7 none repeat scroll 0%;
  border-bottom:1px solid #06289B;
  color:#C9D5F8;
}
#bannersub table {
  width:100%;
}
#bannersub td {
  color:#C9D5F8;
  font-size:80%;
  text-align:center;
}
#bannersub td a {
  color:#C9D5F8;
  text-decoration:none;
}
#bannersub td a.username {
  text-decoration:underline;
}

div.body {
  margin:20px 5% 10px;
  padding-bottom:1em;
}
div.header {
  border-bottom:1px dashed #C9D5F8;
  margin-bottom:15px;
  margin-top:15px;
  padding-bottom:5px;
}
div.header h2 {
  display:inline;
  font-size:120%;
  margin-bottom:8px;
  padding-bottom:5px;
  padding-left:25px;
}
div.main {
  display:block;
  z-index:2;
}
dl.bookmarklist {
  display:block;
  line-height:1.2em;
  padding:0pt;
}
dl.bookmarklist dl, dl.bookmarklist dt, dl.bookmarklist dd {
  display:block;
  margin:0pt;
  padding:0pt;
}
dl.bookmarklist dt.bookmark {
  display:list-item;
  font-weight:normal;
  list-style-type:none;
  margin:1.2em 0pt 0pt;
}
dl.bookmarklist dd {
  margin:0pt 0pt 0pt 1em;
}
dl.bookmarklist dd.comment {
  color:green;
  font-size:80%;
  margin-top:3px;
  padding:0px;
}

app/views/layouts/user.rhtmlを作成

<head>
  <title>Page: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'user' %>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield  %>
</body>

app/views/user/show.rhtmlを編集

<div id="bannersub">
  <table cellspacing="0">
    <tbody>
      <tr>
        <td width="80%" nowrap="nowrap" style="text-align: left;"> ようこそ<%= link_to(h(@myuser.login), :controller => "user", :action => "show") %>さん </td>
        <td nowrap="nowrap"><%= link_to(h("ログアウト"), :controller => "account", :action => "logout") %></td>
      </tr>
    </tbody>
  </table>
</div>
<div class="body">
<div class="header"><h2><%= h(@user.login) %>さんのブックマーク</h2></div>
<div class="main">
<% for bookmark in @user.bookmarks.find(:all, :include => [:page]) -%>
  <dl class="bookmarklist">
    <dt class="bookmark"><%= link_to(h(bookmark.page.title), :controller => "page", :action => "show", :uri => bookmark.page.uri) %></dt>
    <dd class="comment">
      <span class="comment">
        <%= bookmark.created_at.strftime("%Y年%m月%d日") %>
        <%= h(bookmark.comment) %>
      </span>
    </dd>
    
  </dl>
<% end -%>
</div>
</div>

■bookmarkビューを編集

public/stylesheets/bookmark.cssを作成

body {
  background:#FFFFFF none repeat scroll 0%;
  color:#000000;
  margin:0px;
  padding:0px;
}

#bannersub {
  background:#5279E7 none repeat scroll 0%;
  border-bottom:1px solid #06289B;
  color:#C9D5F8;
}
#bannersub table {
  width:100%;
}
#bannersub td {
  color:#C9D5F8;
  font-size:80%;
  text-align:center;
}
#bannersub td a {
  color:#C9D5F8;
  text-decoration:none;
}
#bannersub td a.username {
  text-decoration:underline;
}

#container {
  margin-left:10px;
  margin-right:10px;
  margin-top:20px;
}

#body {
  margin-left:0px;
}
#body, div.body {
  margin-left:4%;
  margin-right:4%;
  z-index:2;
}

h2 {
  font-size:100%;
  margin-bottom:8px;
  padding-bottom:5px;
  padding-left:25px;
}
h2.ftitle, h2.title {
  border-bottom:1px dashed #C9D5F8;
  font-size:120%;
}

div.info {
  margin-bottom:10px;
  margin-left:10px;
}
.info td {
  font-size:80%;
}
td.label {
  background:#F0F0FF none repeat scroll 0%;
  color:#000000;
  font-weight:bold;
  padding-left:5px;
  padding-right:5px;
}
.label {
  font-size:90%;
}
.note {
  font-size:80%;
  font-weight:normal;
}

app/views/layouts/bookmark.rhtmlを作成

<head>
  <title>Page: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'bookmark' %>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield  %>
</body>

app/views/bookmark/add.rhtmlを編集

<div id="bannersub">
  <table cellspacing="0">
    <tbody>
      <tr>
        <td width="80%" nowrap="nowrap" style="text-align: left;"> ようこそ<%= link_to(h(@myuser.login), :controller => "user", :action => "show") %>さん </td>
        <td nowrap="nowrap"><%= link_to(h("ログアウト"), :controller => "account", :action => "logout") %></td>
      </tr>
    </tbody>
  </table>
</div>

<% if request.post? -%>
<%= error_messages_for "page" %>
<%= error_messages_for "bookmark" %>
<% end -%>

<div id="container">
<div id="body">
<h2 class="title">ブックマークの登録</h2>
<%= form_tag %>
  <div class="info">
    <table><tbody>
      <tr>
        <td nowrap="" class="label">URI</td>
        <td class="addurl"><%= text_field_tag "uri", @page.uri, :size => 40 %></td>
      </tr>
      <tr>
        <td nowrap="" class="label">タイトル<br/><span class="note">(省略可)</span></td>
        <td>
          <%= text_field_tag "title", @page.title, :size => 40 %>
          <span id="comment_count"/>
          <div id="candidates_list" style="height: 40px;"/>
        </td>
      </tr>
      <tr>
        <td nowrap="" class="label">コメント<br/><span class="note">(省略可)</span></td>
        <td>
          <%= text_field_tag "comment", @bookmark.comment, :size => 40 %>
          <span id="comment_count"/>
          <div id="candidates_list" style="height: 40px;"/>
        </td>
      </tr>
    </tbody></table>
  </div>
<p><%= submit_tag %></p>
<%= end_form_tag %>
</div>
</div>

なんちゃってはてなブックマーク完成!

今度こそ本当におつかれさまでした。


追記

思いがけずたくさんのブックマークをいただき大変嬉しく思います。みなさまありがとうございます。

デモとして使いやすいようにプレーンHTMLを用意しました。(見た目のはてブ化は除いています)

↓のリンク先をローカルに保存してご利用下さい。

http://poohkid.googlepages.com/rails_sbs_02.htm

こんな方に役立つかと思います。

  • ネットを参照しながらだと自分が作ったように振る舞いにくい
  • Webブラウザからのコピペが大好きなので、テキストファイルで参照したくない
  • デモに使いたいが、このサイトの雰囲気は会場のお堅い空気にそぐわない
  • デモに使いたいが、会場でネットに接続できなくて困った(acts_as_authenticatedは予めインストールしてね、vendor下のディレクトリコピーで退避できるよ)

id:gomisさん

acts_as_taggable使ってタグ付けまで実装するのはしんどいでしょうか

acts_as_taggableについては全く認識しておりませんでした。

これから調べて可能なら実装するということでお許し下さいませ。


文書で見るより実際に作ってみた方が断然おもしろいので、ぜひチャレンジしてみて下さい!

チャレンジのきっかけが掴めない方は勉強会などに参加すると良いんじゃないかな♪

hibariyahibariya 2008/06/24 14:48 参考になりました!

PoohKidPoohKid 2008/06/24 18:17 既に古い情報となりつつある記事を読んで頂きありがとうございます。
今ですと

Four Days on Rails 2.0
http://rails.to/pages/4daysonrails2

こちらなど旬でオススメでございますよ!

ゲスト