非IT系エンジニアであるmacjがRuby on Railsを学びながらWebサービスを育てる企画。第2回です。サイトの更新自体は随分前に終わってましたが、まとめるのに時間がかかってしまいました。
過去記事:
Ruby on Rails+Herokuで素人が工数12時間でWebサービスを公開するまでの記録
[Railsアプリを育てる#1] Foursquareから取得した位置情報をGoogleマップに静的に表示させる
やりたかったこと
今回は、公開を開始してからすぐに追加しようと思いながらもなかなか出来ずにいたことにチャレンジしました。各バス停(busstopモデル)の時刻表表示(busstopモデルのshowアクション/view)で時刻(timecell)をクリックした際に、
- timecellのshowアクションを開いて「◯◯をHH:MMに発車するバス」という見出しをつけて
- そのバスが各バス停を通過する時間を表示させる
ということ。
最終的にはtimecellのshowアクションではなくbusのshowで実現しましたが(これは後述)、とりあえず目的以上の内容を表示をさせることに成功しました。
今回身につけたこと
試行錯誤のすえ身につけたこと。- テーブル間参照
- リンクを使ったパラメータ渡し
- パラメータに応じてViewの見せ方を変える
- Twitter Bootstrapでiconとボタンを表示させる
データ構造と必要な処理
▼テーブルの関係はこんな感じ。やっとまとめてみた。
というわけで、最初にいろんなサイトを覗きながら想像した必要な処理は以下の2つです。
- timecellが属するバス停(timecell.busstop_id)の名前を取得する
- timecellが属するバス(timecell.bus_id)と同じバスに属する(timecell.bus.timecell ??)を取得する
これをひとつずつ攻めてみます。
belongs_toで示す参照先テーブルからの値取得
まず、timecellが属するバス停の名前を取得する件。本質的に何をやればいいのか、なかなか理解できずにいました。それはリレーショナルデータベースそのものを理解していなかったから。「外部キー」という概念とか。そしてさらに混乱を招いたのが、単にRailsで「外部キー」というとforeign_keyを指すようだけれど、それを使う必要はなかったこと。モデルを作るときに、例えばこの場合だど、
rails g model timecell name:string busstop:references bus:references leave_at:time
とreferencesを指定しておけば、foreign_keyは使用しないで済む。ポイントは名前が参照先のテーブル名と一致していること。これがrails流。
あと必要なことは、モデルで以下のように書いておくこと。
class Timecell < ActiveRecord::Base belongs_to :bus belongs_to :busstop attr_accessible :id, :bus_id, :busstop_id, :leave_at default_scope includes([:busstop, :bus]) end
belongs_to/has_manyだけではダメだった。公式ドキュメントを見る限りは不要そうだったけど、includesを追加。
下記、お世話になったサイト。
モデル間の関連でつまずいたとこ。| チュパカブラの勉強日記
[Rails3] belongs_to で関連づけたモデルの属性値でソートするには :include を使うのだ | Rails3 事始め
リンクからのパラメータ渡し
やりかったことの2つ目(各バス停の通過時刻を表示)をどう実装したらいいのか、これまたしばらく悩みました。timecellのshowを使うと、bus_idを取得してさらにそれに属するtimecellを取ってくるというのが一手間なので、busのshowアクションで表示させることにしました。この方針で行くと逆にやらなければならないことは、どのtimecellをクリックして今のページにやってきたかを知る、ということ。
ここで利用したのが、リンクでパラメータを渡す方法です。
リンク元のViewはこんな感じ。
<%= link_to c.leave_at.strftime("%H:%M"), bus_path(c.bus_id, :stop => @busstop.id, :time => c.id) %>
リンク先のコントローラはこちら。
class BusesController < ApplicationController
def show
@busstop = Busstop.find(params[:stop])
@bus = Bus.find(params[:id])
@timecells = Bus.find(params[:id]).timecells
@timecell = Timecell.find(params[:time])
end
end
リンク元から
- バス停(busstop)のidを示す"stop"というパラメータ
- 選択した時刻(timecell)のidを示す、"time"というパラメータ
を渡して、それをもとにコントローラでbusstop, timecellというインスタンス変数(?)を作ってます。モデル本体のbusとそれに属する(belongs_to)timecellsも生成。それらをshow.erb.htmlに渡します。コントローラで使うparamsという文も何気なく理解せずidを渡していたけれど、ようやく理解出来ました。
受け取ったviewでの処理はこちら。
<%= @busstop.name %>を<%= @timecell.leave_at.strftime("%H:%M") %> に出発するバス
たったこれだけの表示に随分苦労しました。
お気づきかと思いますが、パラメータで渡した@busstopを使っているので、テーブル間参照を活用したtimecell.busstop.nameを使う必要がなくなってしまいました。この部分はパラメータを使わず、timecell.busstop.nameとするのが話の筋としては通ります。
ということで、方針転換(取り組むViewの変更)したり、無理やりな実装もありますが、うまく表示できました。
さらなる機能追加
パラメータ渡しに味をしめて、さらなる機能追加をしました。 上記の「◯◯をHH:MMに発車するバス」の「XX本後」「YY本前」「元のバスに戻る」を表示させることにしました。 まずはbusモデルに始発バスと最終バスを示すbooleanの属性を追加しました。カラムの追加は前回もやったので詳細は省略。実際の処理内容として、編集するのはbusのcontrollerとviewがメイン。
先ほどのbusコントローラで、何本前/後を示すoffsetというパラメータを受け取ることにしました。offsetが正の場合は「後」、負の場合は「前」を示します。
class BusesController < ApplicationController
def show
@offset = params[:offset].to_i
@busstop = Busstop.find(params[:stop])
@bus = Bus.find(params[:id])
@timecells = Bus.find(params[:id]).timecells
@timecell = Timecell.find(params[:time])
end
end
Viewはこんな感じ。offsetによる条件分岐を追加して、見出しの表示を切り替え。
<h3>
<% if @offset==0 %>
<%= @busstop.name %>を<%= @timecell.leave_at.strftime("%H:%M") %> に出発するバス
<% elsif @offset>0 %>
<%= @busstop.name %>を<%= @timecell.leave_at.strftime("%H:%M") %> に出発するバスの<%= @offset %>本後
<% else %>
<%= @busstop.name %>を<%= @timecell.leave_at.strftime("%H:%M") %> に出発するバスの<%= -@offset %>本前
<% end %>
</h3>
ボタンはTwitter Bootstrapを使って、iconとbuttonを生成しました。
後のバスを選ぶたびにoffsetを+1して、前のバスを選ぶたびにoffsetを-1、元のバスに戻る時はoffsetを0にする、という処理を追加しました。timeとstopには引き続き同じものを渡します。
<% if @bus.is_first == false %>
<%= link_to raw("<i class=\"icon-step-backward\"></i> 前のバス"),
bus_path(@bus.id-1, :stop => @busstop.id, :time => @timecell.id, :offset => @offset-1),
class: "btn btn-large" %>
<% else %>
<button class="btn btn-large btn-disabled" disabled><i class="icon-step-backward"></i> 前のバス</button>
<% end %>
<% if @offset == 0 %>
<button class="btn btn-large btn-disabled" disabled>元のバスに戻る</button>
<% else %>
<%= link_to raw("元のバスに戻る"),
bus_path(@bus.id-@offset, :stop => @busstop.id, :time => @timecell.id, :offset => 0),
class: "btn btn-large" %>
<% end %>
<% if @bus.is_last == false %>
<%= link_to raw("後のバス <i class=\"icon-step-forward\"></i>"),
bus_path(@bus.id+1, :stop => @busstop.id, :time => @timecell.id, :offset => @offset+1),
class: "btn btn-large" %>
<% else %>
<button class="btn btn-large btn-disabled" disabled>後のバス <i class="icon-step-forward"></i></button>
<% end %>
勢いで書いたから汚いけど、処理としては簡単な内容かと。
また、注意すべきはbusのidが連番になっていることを仮定して、ボタンを押した際に表示するbus.idを決めていること。(bus.idの+1/-1とoffsetを引いているところ)データベースを更新するときには忘れないようにしないと。
最後にもともとbusのshowに飛んでくるときにoffsetを渡してなくてエラーにならないように、busstopのshowのパラメータ渡しにoffsetを追加して完了。(busのコントローラ側でoffsetがないときの処理をするのがいいんだろうけど)
できたサイト
これで冒頭に出てきた見た目ができました。 実物はこちら:http://sumimaru-go.com/buses/105?offset=4&stop=12&time=10112 (offset=4:4本後のバス, stop=12:押上駅, time=10112:押上駅を07:20に発車するバス)よし今回もやりきった!
