Agile Web Development with Rails, Edition 4

13.1 Iteration H1: Email Notifications 12.2 Iteration G2: Atom Feeds

12.4 Playtime

Add the xml format to the controller

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    @latest_order = @product.orders.order(:updated_at).last
    if stale?(etag: @latest_order, last_modified: @latest_order.created_at.utc)
      respond_to do |format|
        format.xml { render :xml => @product }
        format.atom
      end
    end
  end

Fetch the XML, see that there are no orders there

curl --max-time 15 --silent --user dave:secret http://localhost:3000/products/2/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/2/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/2/who_bought.atom"/>
  <title>Who bought CoffeeScript</title>
  <updated>2014-02-04T20:00:07Z</updated>
  <entry>
    <id>tag:localhost,2005:Order/1</id>
    <published>2014-02-04T20:00:07Z</published>
    <updated>2014-02-04T20:00:07Z</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>CoffeeScript</td>
            <td>1</td>
            <td>$36.00</td>
          </tr>
          <tr>
            <th colspan="2">total</th>
            <th>$36.00</th>
          </tr>
        </table>
        <p>Paid by Check</p>
      </div>
    </summary>
    <author>
      <name>Dave Thomas</name>
      <email>customer@example.com</email>
    </author>
  </entry>
</feed>

Include "orders" in the response

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    @latest_order = @product.orders.order(:updated_at).last
    if stale?(etag: @latest_order, last_modified: @latest_order.created_at.utc)
      respond_to do |format|
        format.xml { render xml: @product.to_xml(include: :orders) }
        format.atom
      end
    end
  end

Fetch the xml, see that the orders are included

curl --max-time 15 --silent --user dave:secret http://localhost:3000/products/2/who_bought.xml
<?xml version="1.0" encoding="UTF-8"?>
<product>
  <id type="integer">2</id>
  <title>CoffeeScript</title>
  <description>&lt;p&gt;
        CoffeeScript is JavaScript done right. It provides all of JavaScript's
	functionality wrapped in a cleaner, more succinct syntax. In the first
	book on this exciting new language, CoffeeScript guru Trevor Burnham
	shows you how to hold onto all the power and flexibility of JavaScript
	while writing clearer, cleaner, and safer code.
      &lt;/p&gt;</description>
  <image-url>cs.jpg</image-url>
  <price type="decimal">36.0</price>
  <created-at type="datetime">2014-02-04T19:56:50Z</created-at>
  <updated-at type="datetime">2014-02-04T19:56:50Z</updated-at>
  <orders type="array">
    <order>
      <id type="integer">1</id>
      <name>Dave Thomas</name>
      <address>123 Main St</address>
      <email>customer@example.com</email>
      <pay-type>Check</pay-type>
      <created-at type="datetime">2014-02-04T20:00:07Z</created-at>
      <updated-at type="datetime">2014-02-04T20:00:07Z</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])
    @latest_order = @product.orders.order(:updated_at).last
    if stale?(etag: @latest_order, last_modified: @latest_order.created_at.utc)
      respond_to do |format|
        format.html
        format.xml { render xml: @product.to_xml(include: :orders) }
        format.atom
      end
    end
  end

See the (raw) HTML

curl --max-time 15 --silent --user dave:secret http://localhost:3000/products/2/who_bought
<!-- START:head -->
<!DOCTYPE html>
<html>
<head>
  <title>Pragprog Books Online Store</title>
  <link data-turbolinks-track="true" href="/assets/application.css?body=1" media="all" rel="stylesheet" type="text/css" />
<link data-turbolinks-track="true" href="/assets/carts.css?body=1" media="all" rel="stylesheet" type="text/css" />
<link data-turbolinks-track="true" href="/assets/line_items.css?body=1" media="all" rel="stylesheet" type="text/css" />
<link data-turbolinks-track="true" href="/assets/orders.css?body=1" media="all" rel="stylesheet" type="text/css" />
<link data-turbolinks-track="true" href="/assets/products.css?body=1" media="all" rel="stylesheet" type="text/css" />
<link data-turbolinks-track="true" href="/assets/scaffolds.css?body=1" media="all" rel="stylesheet" type="text/css" />
<link data-turbolinks-track="true" href="/assets/store.css?body=1" media="all" rel="stylesheet" type="text/css" />
  <script data-turbolinks-track="true" src="/assets/jquery.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/jquery.ui.effect.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/jquery.ui.effect-blind.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/jquery_ujs.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/carts.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/line_items.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/orders.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/products.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/store.js?body=1" type="text/javascript"></script>
<script data-turbolinks-track="true" src="/assets/application.js?body=1" type="text/javascript"></script>
  <meta content="authenticity_token" name="csrf-param" />
<meta content="4aRJKBZPwpvcuZWUYLSN6LKY4FQiNFt0CN+fNNYS8bM=" name="csrf-token" />
</head>
<!-- END:head -->
<body class="products">
  <div id="banner">
    <img alt="Logo" src="/assets/logo.png" />
    Pragmatic Bookshelf
  </div>
  <div id="columns">
    <div id="side">
<!-- START_HIGHLIGHT -->
      <!-- START:hidden_div -->
<!-- START_HIGHLIGHT -->
<!-- END_HIGHLIGHT -->
    <!-- END:hidden_div -->
 
<!-- END_HIGHLIGHT -->
      <ul>
        <li><a href="http://www....">Home</a></li>
        <li><a href="http://www..../faq">Questions</a></li>
        <li><a href="http://www..../news">News</a></li>
        <li><a href="http://www..../contact">Contact</a></li>
      </ul>
    </div>
    <div id="main">
      <h3>People Who Bought CoffeeScript</h3>
 
<ul>
    <li>
      <a href="mailto:customer@example.com">Dave Thomas</a>
    </li>
</ul>
 
    </div>
  </div>
</body>
</html>

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

edit app/controllers/products_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    @latest_order = @product.orders.order(:updated_at).last
    if stale?(etag: @latest_order, last_modified: @latest_order.created_at.utc)
      respond_to do |format|
        format.html
        format.xml { render xml: @product.to_xml(include: :orders) }
        format.atom
        format.json { render json: @product.to_json(include: :orders) }
      end
    end
  end

Fetch the data in JSON format

curl --max-time 15 --silent --user dave:secret http://localhost:3000/products/2/who_bought.json
{"created_at":"2014-02-04T19:56:50Z","description":"<p>\n        CoffeeScript is JavaScript done right. It provides all of JavaScript's\n\tfunctionality wrapped in a cleaner, more succinct syntax. In the first\n\tbook on this exciting new language, CoffeeScript guru Trevor Burnham\n\tshows you how to hold onto all the power and flexibility of JavaScript\n\twhile writing clearer, cleaner, and safer code.\n      </p>","id":2,"image_url":"cs.jpg","price":"36.0","title":"CoffeeScript","updated_at":"2014-02-04T19:56:50Z","orders":[{"address":"123 Main St","created_at":"2014-02-04T20:00:07Z","email":"customer@example.com","id":1,"name":"Dave Thomas","pay_type":"Check","updated_at":"2014-02-04T20:00:07Z"}]}

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])
    @latest_order = @product.orders.order(:updated_at).last
    if stale?(etag: @latest_order, last_modified: @latest_order.created_at.utc)
      respond_to do |format|
        format.html
        format.xml
        format.atom
        format.json { render json: @product.to_json(include: :orders) }
      end
    end
  end

Fetch the (much streamlined) XML

curl --max-time 15 --silent --user dave:secret http://localhost:3000/products/2/who_bought.xml
<order_list for_product="CoffeeScript">
  <order>
    <name>Dave Thomas</name>
    <email>customer@example.com</email>
  </order>
</order_list>

Verify that the tests still pass

rake test
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
Loaded suite /home/rubys/.rvm/gems/ruby-1.9.2-p320/gems/rake-10.1.1/lib/rake/rake_test_loader
Started
 
CartTest:
     PASS add duplicate product (0.25s) 
     PASS add unique products (0.01s) 
 
ProductTest:
     PASS image url (0.02s) 
     PASS product attributes must not be empty (0.00s) 
     PASS product is not valid without a unique title (0.00s) 
     PASS product is not valid without a unique title - i18n (0.00s) 
     PASS product price must be positive (0.00s) 
 
Finished in 0.315174 seconds.
 
7 tests, 28 assertions, 0 failures, 0 errors, 0 skips
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
Loaded suite /home/rubys/.rvm/gems/ruby-1.9.2-p320/gems/rake-10.1.1/lib/rake/rake_test_loader
Started
 
CartsControllerTest:
     PASS should create cart (0.23s) 
     PASS should destroy cart (0.07s) 
     PASS should get edit (0.05s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show cart (0.01s) 
     PASS should update cart (0.01s) 
 
LineItemsControllerTest:
     PASS should create line item (0.03s) 
     PASS should create line item via ajax (0.08s) 
     PASS should destroy line item (0.00s) 
     PASS should get edit (0.01s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show line item (0.01s) 
     PASS should update line item (0.01s) 
 
OrdersControllerTest:
     PASS requires item in cart (0.01s) 
     PASS should create order (0.01s) 
     PASS should destroy order (0.01s) 
     PASS should get edit (0.01s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show order (0.00s) 
     PASS should update order (0.01s) 
 
ProductsControllerTest:
     PASS can't delete product in cart (0.01s) 
     PASS should create product (0.04s) 
     PASS should destroy product (0.00s) 
     PASS should get edit (0.01s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show product (0.01s) 
     PASS should update product (0.01s) 
 
StoreControllerTest:
     PASS markup needed for store.js.coffee is in place (0.02s) 
     PASS should get index (0.01s) 
 
Finished in 0.726352 seconds.
 
33 tests, 54 assertions, 0 failures, 0 errors, 0 skips

Commit

git commit -a -m "Orders"
[master a0c1c40] Orders
 4 files changed, 43 insertions(+), 21 deletions(-)
git tag iteration-g

13.1 Iteration H1: Email Notifications 12.2 Iteration G2: Atom Feeds