Webフレームワークの新しい波 Waves探訪記

第4回Twitter風WebアプリケーションTwを強化する

コントローラを作る

前回までに、ビューを作るかデフォルトを修正するだけで、Wavesは様々なことができることを示しました。今回は一歩進んで独自のモデルやコントローラを作ってみます。

まずはWordコントローラを作ってみましょう。前回、controllers/default.rbを書き換えて、nameフィールドではなく、idフィールドでレコードを探すように修正しましたが、これをWordモデル限定の動作にして、他のモデルはWavesのデフォルトである「nameフィールドの値でレコードを見つける」にしたいからです。

default.rbを元に戻す

まず、controllers/default.rbの動作を元に戻してください。

default.rbの前回修正部分
def all; model.order(:created_on.desc).all; end
def find( name ); model[ :id => name ] or not_found; end
default.rbの動作を戻す
def all; model.all; end
def find( name ); model[ :name => name ] or not_found; end

この変更により、Twアプリケーションは正常に動作しなくなります。そこで、以下の修正を行ってください。

Wordコントローラを作る

Wordコントローラを作る
rake generate:controller name=word

生成されたcontrollers/word.rbを上記の逆に修正します。これでWordモデルの検索動作だけがidフィールドに対して行われます。Twアプリケーションも無事動くようになるはずです。

WavesのREST動作

前回も簡単に述べましたが、Wavesがデフォルトで提供しているREST動作やnameフィードでレコードを検索する動作は、デフォルトルールとしてconfigrations/mapping.rbで指定されています。⁠include Waves::Mapping::PrettyUrls::RestRules」「include Waves::Mapping::PrettyUrls::GetRules」です。前者がREST動作を規定して、後者がnameフィールドによる検索を規定しています。デフォルトのルールが不満なら、mapping.rbからルールを削除して、自前で記述することも可能です。

include Waves::Mapping::PrettyUrls::RestRule

RestRuleをincludeすると、以下の3つの処理が使えるようになります。

POST /リソース        # 新しいリソースを追加する
POST /リソース/名前    # 指定の名前のリソースを更新する
DELETE /リソース/名前  # 指定の名前のリソースを削除する

Twアプリケーションで'/words'というURLに対してフォームの内容をPOSTすると、新規にWordレコードが生成されるのが、RestRuleの機能です。

include Waves::Mapping::PrettyUrls::GetRules

GetRulesをincludeすると、以下の処理が使えるようになります。

/リソース(複数形)      # リソースの全リストを取得する
/リソース/名前        # 指定の名前のリソースを取得する
/リソース/名前/editor # 指定のリソースの編集画面を呼び出す

Twアプリケーションで'/words'というURLを呼び出すと、Wordテーブルの全レコードを取得して、templates/word/list.mabが呼び出されるのがGetRulesの機能です。

ユーザと関連を追加する

Twアプリケーションは今のままではアクセスした人の発言がそのままリストされるだけなので、ユーザ毎に発言をまとめる機能を追加します。そのためにUserモデルを追加して、ユーザと発言の関連も追加します。

Userモデルを追加する

次に認証を追加することを考慮して、ユーザ名とパスワードを格納するテーブルを作ります。

Userテーブルのマイグレーションを生成する
rake schema:migration name=add_user

schema/002_add_users.rbを以下のように修正します。

Userテーブルのマイグレーションを修正する
class AddUsers < Sequel::Migration
  def up
    create_table :users do
      primary_key :id
      text :name, :unique => true, :null => false
      text :password, :null => false
    end
    add_column :words, :user_id, :foreign_key, :table => :users
  end
  def down
    drop_table :users
    drop_column :words, :user_id
  end
end

Usersテーブルはid、name、passwordフィールドを持ちます。nameフィールドはユニークでなければなりません。また、発言は発言者に属するので、Wordsテーブルにuser_idフィールドを追加します。⁠rake schema:migrate」でマイグレーションを実行しましょう。テーブルが生成されます。次にWordモデルとUserモデルを作り、両者の関連を設定します。

WordモデルとUserモデル
rake generate:model name=word
rake generate:model name=user

models/word.rbを以下のように修正します。

Wordモデルにmany_to_oneを設定する
module Tw
  module Models
    class Word < Sequel::Model(:words)
      many_to_one :user, :class=>Tw::Models::User
      after_create do
        set(:created_on => Time.now) if columns.include? :created_on
      end
      before_save do
        set(:updated_on => Time.now) if columns.include? :updated_on
      end
    end
  end
end

一人のユーザが複数の発言を所有するので「many_to_one :user, :class=>Tw::Models::User」が必要になります。Railsと異なり、クラスまで指定する必要があります。逆にUserモデル(models/user.rb)には、⁠one_to_many :words, :class=>Tw::Models::Word」を追加してください。これでUserモデルとWordモデルの間に関連が設定されました。

認証を追加する

認証機能を追加して、発言者が分かるようにします。Wavesの認証に関しては明示的に書かれたドキュメントが無く、お勧めのやり方がどれかはよく分からないので、ここではRackのBASIC認証を使ってみます。

設定ファイル(configrations/development.rb)の修正

RackはWavesでWebサーバの抽象化を担うライブラリです。configrations/development.rbを以下のように変更することで、簡単なBASIC認証を使うことができます。

BASIC認証を行う
module Tw
  module Configurations
    class Development < Default
      
      database :host=>'localhost', :adapter=>'sqlite', :database=>'db/tw_dev'
      host '0.0.0.0'
      port 3000
      reloadable [ Tw ]
      log :level => :debug  
      
      application do
        use Rack::Auth::Basic do |username, password|
          u = Tw::Models::User.find(:name=>username)
          u && u.password == password
        end
        use Rack::Static, :urls => ['/css', '/javascript'], :root => 'public'
        use Rack::ShowExceptions
        run Waves::Dispatchers::Default.new
      end       
      
    end
  end
end

「use Rack::Auth::Basic」以下のブロック内で、Userモデルの名前とパスワードの一致を確認しています。

スタティックファイルの処理

また「use Rack::Static, :urls => ['/css', '/javascript'], :root => 'public'」を追加しています。Rack::StaticはRackのミドルウェアのひとつで、指定したディレクトリ配下のファイルはWavesに渡されず、即座にクライアントにレスポンスとして返されます。スタイルシートのように内容の変化しない静的なファイルの指定に利用します。

さてこれでとりあえず認証画面が出るようになったはずです。ブラウザから「http://localhost:3000/words」と入力してみてください。IDとパスワードの要求画面になれば成功です。

ビューを修正する

ユーザと発言には一対多の関連を設定しました。次に発言をPOSTするとき、それがどのユーザのものであるか指定する必要があります。発言をPOSTするビュー(templates/word/list.mab)を修正します。

発言をPOSTするときにユーザidを設定する

ユーザを指定して発言をPOSTするコード
layout :default, :title=>'Tw' do
  @user = Tw::Models::User[:name=>request.env['REMOTE_USER']]
  form :action=>'/words', :method=>'post' do
    textarea '', :name=>'word.text', :cols=>80, :class=>'words'; br
    input :type=>:hidden, :name=>'word.user_id', :value=>@user.id
    input :type=>:submit, :value=>'Update'
  end
  @words.each do |word|
    view :word, :summary, :word=>word, :user=>@user
  end
end

まず「@user = Tw::Models::User[:name=>request.env['REMOTE_USER']]」でユーザを取得しています(ユーザのチェックをページ表示ごとに毎回行うのは効率が悪いので、Rack::Auth::Basicで認証を行ったときにセッションで格納するなどの手段をとるべきかもしれません⁠⁠。そして「input :type=>:hidden, :name=>'word.user_id', :value=>@user.id」にあるようにhidden要素の名前を'word.user_id'、値を@user.idにすることで、wordレコードのuser_idフィールドに該当ユーザのidがセットされます。

ここで/wordsに対してPOSTすればレコードの生成になるのは前述したとおりです。

またsummaryテンプレートに「view :word, :summary, :word=>word, :user=>@user」と@userを渡すことで、各発言と一緒にユーザ名も表示できるようになりますので、summaryテンプレート(templates/word/summary.mab)の修正を行います。cssの組み込みを考えて、クラス名なども設定しましょう。

Markabyテンプレート

ここまでWavesのデフォルトのテンプレートライブラリであるMarkabyについて、あまり説明してこなかったので、ここで簡単に説明します。通常のテンプレートライブラリでは、特殊なテンプレート用の記法を導入しているものがほとんどです。例えばRailsのテンプレートエンジンであるerubyではhtmlのコードの中に「<%= %>」といった特殊な記法を導入し、その中でRubyの式を書きます。

Markabyはこうした独自の記法をまったく使わず、全てRubyのコードで記述します。Markabyの例をドキュメントから引用します。

Markabyのコードの例
html do
  head do
    title 'Products: ' + action_name
    stylesheet_link_tag 'scaffold'
  end

  body do
    p flash[:notice], :style => "color: green"

    self << content_for_layout
  end
end

Markabyでは、Rubyのメソッドして解釈されない名前がhtmlの要素として展開されます。例えばtableメソッドは<table>要素になります。パラメータとしてハッシュを渡すと、html要素のアトリビュートになります。ただし第一引数の文字列は要素にはさまれる文字列として展開されます。 ⁠a 'test', :href=>'/testhost/index'」とすると<a href="/testhost/index">test</a>」となるわけです。ネストする要素はブロック内に記述します。

要素のクラスとIDの指定には特別な記法が用意されています。contentsクラスのdiv要素なら「div.contents⁠⁠、IDがheaderのdiv要素なら「div.header!」と記述します。

Wavesの各コンポーネントはドキュメントが少ないのが弱点なのですが、その上Markabyのように機能を動的に生成するライブラリが多いので、Rdocを見ても記述がないものがたくさんあります。今のところ地道にサンプルコードやソースコードを当たるしかないようです。

summaryテンプレートを修正する

以下のように修正します。各要素に適当なクラスを設定して、cssでデザインを指定できるようにします。list.mabのviewメソッドで呼び出したときに@wordと@userを設定しているので、この中で利用しています。

summaryテンプレート
div.text do
  table(:class=>(@word.user.name==@user.name)?'mywords':'words') do
    tr do
      td.name(:rowspan=>2, :style=>'width:20%;') {@word.user.name}
      td.word {@word.text}
    end
    tr do
      td.date {@word.created_on.strftime("%Y-%m-%d %H:%M") if @word.created_on}
    end
  end
end

あとは適当なcssを設定すれば見栄えを変えることができるようになりました。 public/css/base.cssを作り、簡単に設定しておきます。

public/css/base.css
body {
  padding: 2em;
}

table.words {
  margin: 2px;
  width: 600px;
  border: 1px dotted orange;
}

table.mywords {
  margin: 2px;
  width: 600px;
  border: 1px solid orange;
}

table.words tr {}

table.words td,
table.mywords td {
  padding: 2px 10px;
}

まとめと次回の予定

ユーザの認証、自発言と他人の発言の区別、cssによるデザインの設定などができるようになりました。次回はユーザ間の関連、Twitterで言うfollow/followingのような友人関係を組み込みたいと思います。

おすすめ記事

記事・ニュース一覧