ITコンサルの日常

ITコンサル会社に勤務する普通のITエンジニアの日常です。

「RailsによるアジャイルWebアプリケーション開発」22章読了

Action Viewなお話し。

Builderライブラリを使ってHTMLを構築する(ほぼ意味なし)

products_controller.rbに以下のようなアクションを追加する。

  def builder_test
    @products = Product.find(:all)
  end

で、app/views/products/builder_test.rxmlを作成する

xml.html do
  xml.head do
    xml.title("Products")
  end

  xml.body do
    xml.table(:border => "1") do
      @products.each { |product|
        xml.tr do 
          xml.td(product.title)
          xml.td(product.image_url)
        end
      }
    end
  end
end

でもって、
http://localhost:3000/products/builder_test/1
へアクセス。



一応それらしくは表示される。
が、しかし、HTMLのソースを表示してみると、

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Products: builder_test</title>
  <link href="/stylesheets/scaffold.css?1215862242" media="screen" rel="stylesheet" type="text/css" />
</head>
<body>

<p style="color: green"></p>

<html>

  <head>
    <title>Products</title>
  </head>
  <body>
    <table border="1">
      <tr>
        <td>testtetes</td>
        <td>test</td>

      </tr>
      <tr>
        <td>&#12390;st</td>
        <td>slkdfjasdf.png</td>
      </tr>
    </table>
  </body>
</html>


</body>
</html>

htmlタグが二回出力されちゃってます。。
これは、テンプレートとレイアウトの両方でHTMLタグを書いてしまっているためですね。
すぐに思いつくのは、テンプレート側でレイアウトに書かれたもの以外を出力するという方法ですが、ここではレイアウトを使わない方法を考えたいと思います。


で、探したら、この章の最後の方にありました。
renderをデフォルトの挙動にまかせるのではなく、明示的にレイアウトなしで呼びます。

  def builder_test
    @products = Product.find(:all)
  
    render(:layout => false)
  end

でもって、
http://localhost:3000/products/builder_test/1
へアクセス。


すると、ブラウザにXMLとして認識されてしまいました。
Content-Typeがapplication/xmlになっているのが原因のようです。
ので、Content-Typeをtext/htmlにしてみます。

  def builder_test
    @products = Product.find(:all)
  
    headers["Content-Type"] = "text/html"
    render(:layout => false)
  end

これで出来ました。
ただ、こうしてしまうと、
http://localhost:3000/products/builder_test/1.xml
へアクセスしても、htmlが表示されてしまいますね。


ので、scaffoldされたproducts_controller.rbを参考にすると、

  def builder_test
    @products = Product.find(:all)

    respond_to do |format|
      format.html {
        headers["Content-Type"] = "text/html"
        render(:layout => false)
      }
      format.xml  # builder_test.rxml
    end
  end

のようにするのが正しいように思えます。
こうすると、
/builder_test/1 → レイアウトなしのHTML
/builder_test/1.html → レイアウトなしのHTML(同上)
/builder_test/1.xmlXML
となり、期待通りになっているようです。


でもまあ、素直にerb使えよって感じです。

ポップアップウインドウ内にレスポンスを表示する

index.html.erbに以下の行を追加する。

  <tr>
    <td><%=h product.title %></td>
    <td><%=h product.description %></td>
    <td><%=h product.image_url %></td>
    <td><%= link_to 'Show', product %></td>
    <!-- added -->
    <td><%= link_to 'Popup Show', product, :popup => ['Popup Show', 'width=200, height=150'] %></td>
    <td><%= link_to 'Edit', edit_product_path(product) %></td>
    <td><%= link_to 'Destroy', product, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>

リンクを追加したインデックスぺージと、showアクションをポップアップ表示した結果のイメージ。


画像にリンクを設定する

link_toにimage_tagを組み合わせるだけです。
さっきのindex.html.erbを改造してみる。

  <tr>
    <td><%=h product.title %></td>
    <td><%=h product.description %></td>
    <td><%=h product.image_url %></td>
    <td><%= link_to 'Show', product %></td>
    <!-- added -->s
    <td><%= link_to(image_tag("rails.png", :size => "50x30"), product, :popup => ['Popup Show', 'width=200, height=150']) %></td>
    <td><%= link_to 'Edit', edit_product_path(product) %></td>
    <td><%= link_to 'Destroy', product, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>

で、こうなる。


国とタイムゾーンの選択リストを作る

説明が省略されていたので作ってみる。


まずはコントローラ。
products_controller.rbに以下のアクションを追加。

  def country_timezone
  end

アクションを受け付けて、規定のテンプレートをレンダリングするだけのつまらないアクションです。


次にビュー。
app/views/products/country_timezone.html.erbを作成。

<h1>Choise Country and Timezone</h1>

<% form_tag :action => :country_timezone, :id => 1 do |f| %>

  <%= select_tag("country", country_options_for_select(params[:country])) %>

  <%= select_tag("timezone", time_zone_options_for_select(params[:timezone])) %>

  <%= submit_tag %>
<% end %>

<p>
selected country: <%= params[:country] %><br>
selected timezone: <%= params[:timezone] %><br>

これで、
http://localhost:3000/products/country_timezone/1
にアクセスすると、こんな感じに表示される。


プルダウンから国とタイムゾーンを選択し、Save Changesを押下するたびに、
プルダウンの中身と、下のテキストの内容が変化します。

ファイルのアップロード

書籍中では、データベースにアップロードしたファイルを格納する例が書かれているので、ここではデータベースに格納しない例を作ってみます。


まずは、アップロードフォームを表示するアクションと、アップロードを行うアクションを定義。
app/controllers/products_controller.rb

  def new_upload
  end

  def upload
    fileobj = params["uploaded_file"]
    puts fileobj.class.to_s
    puts fileobj.content_type
    
    redirect_to :action => :new_upload
  end

new_uploadは、上のと同じで、アクションを受け付けて、規定のテンプレートをレンダリングするだけのつまらないアクションです。
uploadの方は、パラメータ(uploaded_file; フォームの名前)で受け取った値が、どの型であるかと、Content-Typeをサーバのコンソールに表示しています。(実際にファイルの保存はしません。)
表示が終わったら、アップロードフォームに戻るようリダイレクトします。


IDなしのURLでもアクセスできるよう、routes.rbにルーティングを追加。

  map.connect ':controller/new_upload', :action => "new_upload"
  map.connect ':controller/upload', :action => "upload"
  map.resources :products

map.resources :productsの上に2行追加。


次に、アップロードフォーム(ビュー)を作成する。
app/views/products/new_upload.html.erb

<h1>File upload sample</h1>

<% form_tag ({:action => :upload},{ :multipart => true }) do |f| %>
  <%= file_field_tag("uploaded_file") %>

  <%= submit_tag "Upload" %>
<% end %>

multipartにするのがミソですね。


ここまで出来たら、
http://localhost:3000/products/new_upload
にアクセスして、アップロードフォームを表示してみる。

適当にファイルを選んで、Uploadボタンをクリックすると、サーバのコンソールに以下のようなものが出力されました。

ActionController::UploadedStringIO
text/plain
127.0.0.1 - - [25/Jul/2008:12:03:36 JST] "POST /products/upload HTTP/1.1" 302 107
http://localhost:3000/products/new_upload -> /products/upload
/home/taka/test/app/views/products/new_upload.html.erb:3: warning: multiple values for a block parameter (0 for 1)
        from /usr/lib/ruby/gems/1.8/gems/actionpack-2.1.0/lib/action_view/helpers/capture_helper.rb:141
127.0.0.1 - - [25/Jul/2008:12:03:37 JST] "GET /products/new_upload HTTP/1.1" 304 0
http://localhost:3000/products/new_upload -> /products/new_upload

アップロードされたファイルは、ActionController::UploadedStringIO型として扱えるようになっているようです。
また、試しにアップロードしてみたファイルはテキストファイルだったので、Content-Typeはtext/plainになってますね。
あとは煮るなり焼くなりご自由に。

フラグメントキャッシュ

動的コンテンツと静的コンテンツが混ざったページにおいて、静的コンテンツ部分のみキャッシュしたいという要望に答えるのがフラグメントキャッシュです。


簡単なサンプルを書いてみます。


いつもの順番で、まずはコントローラ。
app/controllers/products_controller.rb

  def fragment
    @dateTime = Time.now
  end

  def expirefragment
    expire_fragment :action => :fragment, :id => params[:id]
  
    redirect_to :action => :fragment, :id => params[:id]
  end

フラグメントキャッシュを行うページを表示するアクションfragmentと、フラグメントキャッシュをクリアして(失効させて)ぺージを再描画するアクションexpirefragmentを用意しました。


次にビュー。
app/views/products/fragment.html.erb

<%= @dateTime %>

<% cache do %>
<p>
静的コンテンツ
</p>

<%= link_to "expire fragment cache", :action => :expirefragment, :id => params[:id] %>
<% end %>

cacheメソッドに囲まれたブロックが、フラグメントキャッシュされる部分。
それ以外はキャッシュされない部分なので、動的コンテンツなどキャッシュされたくないものを置きます。


ここまで出来たら、
http://localhost:3000/products/fragment/1
にアクセスして、ページを表示してみる。

リロードすると、日時の部分が書き換わります。


で、試しにfragment.html.erbの静的コンテンツ部分を変えてみる。

<%= @dateTime %>

<% cache do %>
<p>
静的コンテンツだよ。
</p>

<%= link_to "expire fragment cache", :action => :expirefragment, :id => params[:id] %>
<% end %>

これでリロードしても、キャッシュされているので表示されている内容は変わりません。
その代わり、expire fragment cacheのリンクをクリックすると、キャッシュがクリアされて、最新の内容になります。