Agile Web Development with Rails, Edition 4

Agile Web Development with Rails, Edition 4

12.3 Iteration G3: Pagination 12.1 Iteration G1: Capturing an Order

12.2 Iteration G2: Atom Feeds

Demonstrate various respond_to/format options, as well as "through" relations and basic authentication.

Define a "who_bought" member action

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    respond_to do |format|
      format.xml { render :xml => @product }
    end
  end

Add to the routes

edit config/routes.rb
Depot::Application.routes.draw do |map|
  resources :orders
 
  resources :line_items
 
  resources :carts
 
  get "store/index"
 
  resources :products do
    get :who_bought, :on => :member
  end
 
 
  # ...
 
  # You can have the root of your site routed with "root"
  # just remember to delete public/index.html.
  # root :to => "welcome#index"
  root :to => 'store#index', :as => 'store'
 
  # ...
end

Try again... success... but not much there

curl --silent --user dave:secret http://localhost:3000/products/3/who_bought.xml
<?xml version="1.0" encoding="UTF-8"?>
<product>
  <created-at type="datetime">2010-06-06T14:32:18Z</created-at>
  <description>&lt;p&gt;
        Ruby is the fastest growing and most exciting dynamic language out
        there. If you need to get working programs delivered fast, you should
        add Ruby to your toolbox.
      &lt;/p&gt;</description>
  <id type="integer">3</id>
  <image-url>/images/ruby.jpg</image-url>
  <price type="decimal">49.5</price>
  <title>Programming Ruby 1.9</title>
  <updated-at type="datetime">2010-06-06T14:32:18Z</updated-at>
</product>

Add "orders" to the Product class

edit app/models/product.rb
class Product < ActiveRecord::Base
  has_many :orders, :through => :line_items
  has_many :line_items
 
  before_destroy :ensure_not_referenced_by_any_line_item
 
  # ensure that there are no line items referencing this product
  def ensure_not_referenced_by_any_line_item
    if line_items.count.zero?
      return true
    else
      errors[:base] << "Line Items present"
      return false
    end
  end
 
  #...

Include "orders" in the response

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    respond_to do |format|
      format.xml { render :xml => @product.to_xml(:include => :orders) }
    end
  end

Fetch the xml, see that the orders are included

curl --silent --user dave:secret http://localhost:3000/products/3/who_bought.xml
<?xml version="1.0" encoding="UTF-8"?>
<product>
  <created-at type="datetime">2010-06-06T14:32:18Z</created-at>
  <description>&lt;p&gt;
        Ruby is the fastest growing and most exciting dynamic language out
        there. If you need to get working programs delivered fast, you should
        add Ruby to your toolbox.
      &lt;/p&gt;</description>
  <id type="integer">3</id>
  <image-url>/images/ruby.jpg</image-url>
  <price type="decimal">49.5</price>
  <title>Programming Ruby 1.9</title>
  <updated-at type="datetime">2010-06-06T14:32:18Z</updated-at>
  <orders type="array">
    <order>
      <address>123 Main St</address>
      <created-at type="datetime">2010-06-06T14:37:41Z</created-at>
      <email>customer@example.com</email>
      <id type="integer">1</id>
      <name>Dave Thomas</name>
      <pay-type>Check</pay-type>
      <updated-at type="datetime">2010-06-06T14:37:41Z</updated-at>
    </order>
  </orders>
</product>

Define an HTML view

edit app/views/products/who_bought.html.erb
<h3>People Who Bought <%= @product.title %></h3>
 
<ul>
  <% for order in @product.orders %>
    <li>
      <%= mail_to order.email, order.name %>
    </li>
  <% end %>
</ul>

Add the html format to the controller

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    respond_to do |format|
      format.html
      format.xml { render :xml => @product.to_xml(:include => :orders) }
    end
  end

See the (raw) HTML

curl --silent --user dave:secret http://localhost:3000/products/3/who_bought
<!DOCTYPE html>
<html>
<head>
  <title>Pragprog Books Online Store</title>
<!-- START:stylesheet -->
  <link href="/stylesheets/scaffold.css?1275834701" media="screen" rel="stylesheet" type="text/css" />
  <link href="/stylesheets/depot.css?1275835058" media="all" rel="stylesheet" type="text/css" />
<!-- END:stylesheet -->
  <script src="/javascripts/prototype.js?1275834690" type="text/javascript"></script>
<script src="/javascripts/effects.js?1275834690" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1275834690" type="text/javascript"></script>
<script src="/javascripts/controls.js?1275834690" type="text/javascript"></script>
<script src="/javascripts/rails.js?1275834690" type="text/javascript"></script>
<script src="/javascripts/application.js?1275834690" type="text/javascript"></script>
  <meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="v5h9x3ZQJdbIS4yQ+3UDSbUxGT1GpvBqlrVJHACSZO8="/>
</head>
<body id="store">
  <div id="banner">
    <img alt="Logo" src="/images/logo.png?1275834738" />
    Pragmatic Bookshelf
  </div>
  <div id="columns">
    <div id="side">
      <!-- START_HIGHLIGHT -->
      <!-- START:hidden_div -->
<!-- START_HIGHLIGHT -->
<!-- END_HIGHLIGHT -->
    <!-- END:hidden_div -->
      <!-- END_HIGHLIGHT -->
 
      <a href="http://www....">Home</a><br />
      <a href="http://www..../faq">Questions</a><br />
      <a href="http://www..../news">News</a><br />
      <a href="http://www..../contact">Contact</a><br />
    </div>
    <div id="main">
      <h3>People Who Bought Programming Ruby 1.9</h3>
 
<ul>
    <li>
      <a href="mailto:customer@example.com">Dave Thomas</a>
    </li>
</ul>
 
    </div>
  </div>
</body>
</html>

Define an Atom view (using the Atom builder)

edit app/views/products/who_bought.atom.builder
atom_feed do |feed|
  feed.title "Who bought #{@product.title}"
 
  latest_order = @product.orders.sort_by(&:updated_at).last
  feed.updated( latest_order && latest_order.updated_at )
 
  for order in @product.orders
    feed.entry(order) do |entry|
      entry.title "Order #{order.id}"
      entry.summary :type => 'xhtml' do |xhtml|
        xhtml.p "Shipped to #{order.address}"
 
        xhtml.table do
          xhtml.tr do
            xhtml.th 'Product'
            xhtml.th 'Quantity'
            xhtml.th 'Total Price'
          end
          for item in order.line_items
            xhtml.tr do
              xhtml.td item.product.title
              xhtml.td item.quantity
              xhtml.td number_to_currency item.total_price
            end
          end
          xhtml.tr do
            xhtml.th 'total', :colspan => 2
            xhtml.th number_to_currency \
              order.line_items.map(&:total_price).sum
          end
        end
 
        xhtml.p "Paid by #{order.pay_type}"
      end
      entry.author do |author|
        entry.name order.name
        entry.email order.email
      end
    end
  end
end

Add the atom format to the controller

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    respond_to do |format|
      format.atom
      format.html
      format.xml { render :xml => @product.to_xml(:include => :orders) }
    end
  end

Fetch the Atom feed

curl --silent --user dave:secret http://localhost:3000/products/3/who_bought.atom
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <id>tag:localhost,2005:/products/3/who_bought</id>
  <link rel="alternate" type="text/html" href="http://localhost:3000"/>
  <link rel="self" type="application/atom+xml" href="http://localhost:3000/products/3/who_bought.atom"/>
  <title>Who bought Programming Ruby 1.9</title>
  <updated>2010-06-06T14:37:41Z</updated>
  <entry>
    <id>tag:localhost,2005:Order/1</id>
    <published>2010-06-06T14:37:41Z</published>
    <updated>2010-06-06T14:37:41Z</updated>
    <link rel="alternate" type="text/html" href="http://localhost:3000/orders/1"/>
    <title>Order 1</title>
    <summary type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <p>Shipped to 123 Main St</p>
        <table>
          <tr>
            <th>Product</th>
            <th>Quantity</th>
            <th>Total Price</th>
          </tr>
          <tr>
            <td>Programming Ruby 1.9</td>
            <td>1</td>
            <td>$49.50</td>
          </tr>
          <tr>
            <th colspan="2">total</th>
            <th>$49.50</th>
          </tr>
        </table>
        <p>Paid by Check</p>
      </div>
    </summary>
    <author>
      <name>Dave Thomas</name>
      <email>customer@example.com</email>
    </author>
  </entry>
</feed>
pub depot_o

Anything that XML can do, JSON can too...

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    respond_to do |format|
      format.atom
      format.html
      format.xml { render :xml => @product.to_xml(:include => :orders) }
      format.json { render :json => @product.to_json(:include => :orders) }
    end
  end

Fetch the data in JSON format

curl --silent --user dave:secret http://localhost:3000/products/3/who_bought.json
{"created_at":"2010-06-06T14:32:18Z","description":"<p>\n        Ruby is the fastest growing and most exciting dynamic language out\n        there. If you need to get working programs delivered fast, you should\n        add Ruby to your toolbox.\n      </p>","id":3,"image_url":"/images/ruby.jpg","price":"49.5","title":"Programming Ruby 1.9","updated_at":"2010-06-06T14:32:18Z","orders":[{"address":"123 Main St","created_at":"2010-06-06T14:37:41Z","email":"customer@example.com","id":1,"name":"Dave Thomas","pay_type":"Check","updated_at":"2010-06-06T14:37:41Z"}]}

Customize the xml

edit app/views/products/who_bought.xml.builder
xml.order_list(:for_product => @product.title) do
  for o in @product.orders
    xml.order do
      xml.name(o.name)
      xml.email(o.email)
    end
  end
end

Change the rendering to use templates

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    respond_to do |format|
      format.atom
      format.html
      format.xml
      format.json { render :json => @product.to_json(:include => :orders) }
    end
  end

Fetch the (much streamlined) XML

curl --silent --user dave:secret http://localhost:3000/products/3/who_bought.xml
<order_list for_product="Programming Ruby 1.9">
  <order>
    <name>Dave Thomas</name>
    <email>customer@example.com</email>
  </order>
</order_list>

Consider reducing the number of edits to products_controller

12.3 Iteration G3: Pagination 12.1 Iteration G1: Capturing an Order