Iteration G2: Atom Feeds

By using a standard feed format, such as Atom, you can immediately take advantage of a wide variety of preexisting clients. Because Rails already knows about IDs, dates, and links, it can free you from having to worry about these pesky details and let you focus on producing a human-readable summary. We start by adding a new action to the products controller:

 def​ ​who_bought
  @product = Product.​find​(params[​:id​])
  @latest_order = @product.​orders​.​order​(​:updated_at​).​last
 if​ stale?(@latest_order)
  respond_to ​do​ |format|
  format.​atom
 end
 end
 end
Joe asks:
Joe asks:
Why Atom?

A number of different feed formats exit—most notably RSS 1.0, RSS 2.0, and Atom, standardized in 2000, 2002, and 2005, respectively. These three are all widely supported. To aid with the transition, a number of sites provide multiple feeds for the same site, but this is no longer necessary, increases user confusion, and generally isn’t recommended.

The Ruby language provides a low-level library that can produce any of these formats as well as a number of other less common versions of RSS. For best results, stick with one of the three main versions.

The Rails framework is all about picking reasonable defaults, and it has chosen Atom as the default for feed formats. It’s specified as an Internet standards--track protocol for the Internet community by the IETF, and Rails provides a higher-level helper named atom_feed that takes care of a number of details based on knowledge of Rails naming conventions for things like IDs and dates.

In addition to fetching the product, we check to see if the request is stale. Remember in Iteration C5: Caching of Partial Results, when we cached partial results of responses because the catalog display was expected to be a high-traffic area? Well, feeds are like that, but with a different usage pattern. Instead of a large number of different clients all requesting the same page, we have a small number of clients repeatedly requesting the same page. You might be familiar with the idea of browser caches; the same concept holds true for feed aggregators.

The way this works is that the responses contain a bit of metadata that identifies when the content was last modified and a hashed value called an ETag. If a subsequent request provides this data back, this gives the server the opportunity to respond with an empty response body and an indication that the data hasn’t been modified.

As is usual with Rails, you don’t need to worry about the mechanics. You just need to identify the source of the content, and Rails does the rest. In this case, we use the last order. Inside the if statement, we process the request normally.

By adding format.atom, we cause Rails to look for a template named who_bought.atom.builder. Such a template can use the generic XML functionality that Builder provides as well as use the knowledge of the Atom feed format that the atom_feed helper provides:

 atom_feed ​do​ |feed|
  feed.​title​ ​"Who bought ​​#{​@product.​title​​}​​"
 
  feed.​updated​ @latest_order.​try​(​:updated_at​)
 
  @product.​orders​.​each​ ​do​ |order|
  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
  order.​line_items​.​each​ ​do​ |item|
  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|
  author.​name​ order.​name
  author.​email​ order.​email
 end
 end
 end
 end

At the overall feed level, we need to provide only two pieces of information: the title and the date of the latest update. If no orders exist, the updated_at value is null, and Rails supplies the current time instead.

Then we iterate over each order associated with this product by calling @product.orders. Products and orders have no direct relationship to each other, though there is an indirect one via line items. A product’s orders would be the orders associated with any of the product’s line items. We could implement that ourselves by creating an orders method, but Rails provides a way to do this for us, since this indirect relationship is a common pattern. The has_many method we used to tell Rails that a product has many line items takes an optional argument named through: that tells Rails to traverse the indirect relationship. In our case, we’ll tell Rails that a product has many orders through its existing line items relationship:

 class​ Product < ApplicationRecord
  has_many ​:line_items
» has_many ​:orders​, ​through: :line_items
 #...
 end

For each order, we provide a title, a summary, and an author. The summary can be full XHTML, and we use this to produce a table of product titles, quantity ordered, and total prices. We follow this table with a paragraph containing the pay_type.

To make this work, we need to define a route. This action will respond to HTTP GET requests and will operate on a member of the collection (in other words, on an individual product) as opposed to the entire collection (which in this case would mean all products):

 Rails.​application​.​routes​.​draw​ ​do
  resources ​:orders
  resources ​:line_items
  resources ​:carts
  root ​'store#index'​, ​as: ​​'store_index'
» resources ​:products​ ​do
» get ​:who_bought​, ​on: :member
»end
 
 # For details on the DSL available within this file, see
 # https://guides.rubyonrails.org/routing.html
 end

We can try it for ourselves:

 depot>​​ ​​curl​​ ​​--silent​​ ​​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 type="text/html" href="http://localhost:3000" rel="alternate"/>
  <link type="application/atom+xml"
  href="http://localhost:3000/info/who_bought/3.atom" rel="self"/>
  <title>Who bought Programming Ruby 1.9</title>
  <updated>2016-01-29T02:31:04Z</updated>
  <entry>
  <id>tag:localhost,2005:Order/1</id>
  <published>2016-01-29T02:31:04Z</published>
  <updated>2016-01-29T02:31:04Z</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>
  ...
  </table>
  <p>Paid by check</p>
  </div>
  </summary>
  <author>
  <name>Dave Thomas</name>
  <email>[email protected]</email>
  </author>
  </entry>
 </feed>

Looks good. Now we can subscribe to this in our favorite feed reader.

Best of all, the customer likes it. We’ve implemented product maintenance, a basic catalog, and a shopping cart, and now we have a simple ordering system. Obviously, we’ll also have to write some kind of fulfillment application, but that can wait for a new iteration. (And that iteration is one that we’ll skip in this book; it doesn’t have much new to say about Rails.)

What We Just Did

In a fairly short amount of time, we did the following:

  • We created a form to capture details for the order and linked it to a new order model.

  • We added validation and used helper methods to display errors to the user.

  • We provided a feed so the administrator can monitor incoming orders.

Playtime

Here’s some stuff to try on your own:

  • Get HTML- and JSON-formatted views working for who_bought requests. Experiment with including the order information in the JSON view by rendering @product.to_json(include: :orders). Do the same thing for XML using ActiveModel::Serializers::Xml.[58]

  • What happens if you click the Checkout button in the sidebar while the checkout screen is already displayed? Can you find a way to disable the button in this circumstance?

  • The list of possible payment types is currently stored as a constant in the Order class. Can you move this list into a database table? Can you still make validation work for the field?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.174.216