Agile Web Development with Rails, Edition 5

14.1 Iteration I1: Email Notifications 12.4 Iteration G2: Downloading an eBook

12.5 Playtime

add activemodel-serializers-xml

edit Gemfile
gem 'activemodel-serializers-xml'
bundle install --local
Resolving dependencies...
Using rake 12.0.0
Using pg 0.19.0
Using ffi 1.9.18
Using concurrent-ruby 1.0.5
Using i18n 0.8.4
Using minitest 5.8.5
Using thread_safe 0.3.6
Using builder 3.2.3
Using erubis 2.7.0
Using mini_portile2 2.1.0
Using rack 2.0.3
Using nio4r 2.1.0
Using websocket-extensions 0.1.2
Using mime-types-data 3.2016.0521
Using arel 7.1.4
Using bundler 1.15.1
Using method_source 0.8.2
Using thor 0.19.4
Using sqlite3 1.3.13
Using puma 3.9.0
Using sass 3.4.24
Using tilt 2.0.7
Using execjs 2.7.0
Using coffee-script-source 1.12.2
Using turbolinks-source 5.0.3
Using multi_json 1.12.1
Using byebug 9.0.6
Using bindex 0.5.0
Using queue_classic 3.2.0.RC1 from source at `/home/rubys/git/queue_classic`
Using rb-inotify 0.9.9 from source at `/home/rubys/git/rb-inotify`
Using tzinfo 1.2.3
Using nokogiri 1.7.2
Using rack-test 0.6.3
Using sprockets 3.7.1
Using websocket-driver 0.6.5
Using mime-types 3.1
Using uglifier 3.2.0
Using coffee-script 2.4.1
Using turbolinks 5.0.1
Using activesupport 5.0.3 from source at `/home/rubys/git/rails`
Using loofah 2.0.3
Using mail 2.6.5
Using rails-dom-testing 2.0.3
Using globalid 0.4.0
Using activemodel 5.0.3 from source at `/home/rubys/git/rails`
Using jbuilder 2.6.4
Using spring 2.0.2
Using rails-html-sanitizer 1.0.3
Using activejob 5.0.3 from source at `/home/rubys/git/rails`
Using activerecord 5.0.3 from source at `/home/rubys/git/rails`
Using actionview 5.0.3 from source at `/home/rubys/git/rails`
Using activemodel-serializers-xml 1.0.1
Using actionpack 5.0.3 from source at `/home/rubys/git/rails`
Using actioncable 5.0.3 from source at `/home/rubys/git/rails`
Using actionmailer 5.0.3 from source at `/home/rubys/git/rails`
Using railties 5.0.3 from source at `/home/rubys/git/rails`
Using sprockets-rails 3.2.0
Using coffee-rails 4.2.2
Using jquery-rails 4.3.1
Using jquery-ui-rails 6.0.1
Using web-console 3.5.1 from source at `/home/rubys/git/web-console`
Using rails 5.0.3 from source at `/home/rubys/git/rails`
Using sass-rails 5.0.6
Bundle complete! 17 Gemfile dependencies, 63 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Restart the server.

include xml serializers

edit app/models/product.rb
require 'active_model/serializers/xml'
class Product < ApplicationRecord
  include ActiveModel::Serializers::Xml
end
edit app/models/order.rb
require 'active_model/serializers/xml'
class Order < ApplicationRecord
  include ActiveModel::Serializers::Xml
end

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?(@latest_order)
      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 Rails, Angular, Postgres, and Bootstrap</title>
  <updated>2017-06-03T04:42:29Z</updated>
  <entry>
    <id>tag:localhost,2005:Order/1</id>
    <published>2017-06-03T04:42:28Z</published>
    <updated>2017-06-03T04:42:28Z</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>Rails, Angular, Postgres, and Bootstrap</td>
            <td>1</td>
            <td>$45.00</td>
          </tr>
          <tr>
            <th colspan="2">total</th>
            <th>$45.00</th>
          </tr>
        </table>
        <p>Paid by Check</p>
      </div>
    </summary>
    <author>
      <name>Dave Thomas</name>
      <email>customer@example.com</email>
    </author>
  </entry>
  <entry>
    <id>tag:localhost,2005:Order/2</id>
    <published>2017-06-03T04:42:29Z</published>
    <updated>2017-06-03T04:42:29Z</updated>
    <link rel="alternate" type="text/html" href="http://localhost:3000/orders/2"/>
    <title>Order 2</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>Rails, Angular, Postgres, and Bootstrap</td>
            <td>1</td>
            <td>$45.00</td>
          </tr>
          <tr>
            <th colspan="2">total</th>
            <th>$45.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?(@latest_order)
      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>Rails, Angular, Postgres, and Bootstrap</title>
  <description>&lt;p&gt;
      &lt;em&gt;Powerful, Effective, and Efficient Full-Stack Web Development&lt;/em&gt;
      As a Rails developer, you care about user experience and performance,
      but you also want simple and maintainable code. Achieve all that by
      embracing the full stack of web development, from styling with
      Bootstrap, building an interactive user interface with AngularJS, to
      storing data quickly and reliably in PostgreSQL. Take a holistic view of
      full-stack development to create usable, high-performing applications,
      and learn to use these technologies effectively in a Ruby on Rails
      environment.
      &lt;/p&gt;</description>
  <image-url>dcbang.jpg</image-url>
  <price type="decimal">45.0</price>
  <created-at type="dateTime">2017-06-03T04:40:46Z</created-at>
  <updated-at type="dateTime">2017-06-03T04:40:46Z</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">2017-06-03T04:42:28Z</created-at>
      <updated-at type="dateTime">2017-06-03T04:42:28Z</updated-at>
    </order>
    <order>
      <id type="integer">2</id>
      <name>Dave Thomas</name>
      <address>123 Main St</address>
      <email>customer@example.com</email>
      <pay-type>Check</pay-type>
      <created-at type="dateTime">2017-06-03T04:42:29Z</created-at>
      <updated-at type="dateTime">2017-06-03T04:42:29Z</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?(@latest_order)
      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
<!DOCTYPE html>
<html>
  <head>
    <title>Pragprog Books Online Store</title>
    <meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="YL06UtTYgLEcwS9HlLZ3+yk5zJ1PalFl0STbRz114DQA5SZNeysjzpbR7XNazXy3dHZdJiFh87mMvgvhTu2FeA==" />
 
    <meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="5AtxoD0cGmnZfqWTZhO5OLkZCRX5dqt0MzEiFkZhF4qEU22/ku+5FlNuZ6eoaLJ05FaYrpd9Cahuq/KwNflyxg==" />
 
    <link rel="stylesheet" media="all" href="/assets/carts.self-49685e3e927e4b175e8c7e66fb14e646c3b5e9066414e43331408e88148e5559.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/line_items.self-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/orders.self-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/products.self-740e238bdf1bdcb21b444ff4b7efb48bf79c143313abf17e95b05798ccb5bcbf.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/scaffolds.self-247a3c3c3b0605ab5f9400b05efe04fa9ada19eede202733c24f9bc6c3af4f13.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/store.self-cb8661d24cb3dfe0b835083063a9b8b180f4ec6fdc39ecad933759542e33805f.css?body=1" data-turbolinks-track="reload" />
<link rel="stylesheet" media="all" href="/assets/application.self-161d3230145c44917bac526f8655ffcd9792d39b0fa2e1cd05cb470a686341a3.css?body=1" data-turbolinks-track="reload" />
    <script src="/assets/jquery.self-bd7ddd393353a8d2480a622e80342adf488fb6006d667e8b42e4c0073393abee.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/jquery-ui/version.self-c8e3d1203da26ea7efdf83c1eabb3f0ba55cb68e463f5ccf0d77bd15ce6a8e61.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/jquery-ui/effect.self-776455da1682afff3a4974146aa96ca840597b879ed3797af0604063527443c6.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/jquery-ui/effects/effect-blind.self-2f8923f5c3073717dad35ac1f9bd4dbc13d2f9c8128f7dbea9819921338271f4.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/jquery_ujs.self-784a997f6726036b1993eb2217c9cb558e1cbb801c6da88105588c56f13b466a.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/turbolinks.self-1d1fddf91adc38ac2045c51f0a3e05ca97d07d24d15a4dcbf705009106489e69.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/action_cable.self-50cf57bdf719a6c60243f570fdc69cc3b8971d9b5d91d1f3e8fb0edde60c63a3.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/action_cable/connection_monitor.self-dc0643774d220a91b7ba63973ddbc09f4a52965221d646c7830a22fb96ab7e58.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/action_cable/connection.self-0847329926e139a5dbf24e5bb3c08e6206e5c071b877d6cad7da20e55501ed2d.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/action_cable/subscriptions.self-5c7dd80f075eacd4fc06ad385498d4c3a354011b505d8a8cef90df105c361c80.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/action_cable/subscription.self-0a82319db058bc79ff11535c02d44534daf61a646a32010e44e671e7181cc742.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/action_cable/consumer.self-def499e29967995b51c5ad790077021a5b1980f2e3330f4ac33010f6813867cd.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/cable.self-6e0514260c1aa76eaf252412ce74e63f68819fd19bf740595f592c5ba4c36537.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/channels/products.self-a36983cdb45f5c31825f7425c6d56e16b232ff19a2fdac5da7fa48917f824ea7.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/carts.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/line_items.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/orders.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/products.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/store.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1" data-turbolinks-track="reload"></script>
<script src="/assets/application.self-b5f8abbb026842e6f126160e1d1b70143792680186f4176fa4ea063de99e552c.js?body=1" data-turbolinks-track="reload"></script>
  </head>
 
  <body class="products">
    <div id="banner">
      <img alt="The Pragmatic Bookshelf" src="/assets/logo-a8906fb6c933f00883d2ad0385ae52201c0e1d1f9235c37f4a5762f02ad80936.svg" />
      <span class="title"></span>
    </div>
    <div id="columns">
      <div id="side">
<!-- START_HIGHLIGHT -->
 
      <!-- 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 Rails, Angular, Postgres, and Bootstrap</h3>
 
<ul>
    <li>
      <a href="mailto:customer@example.com">Dave Thomas</a>
    </li>
    <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?(@latest_order)
      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
{"id":2,"title":"Rails, Angular, Postgres, and Bootstrap","description":"\u003cp\u003e\n      \u003cem\u003ePowerful, Effective, and Efficient Full-Stack Web Development\u003c/em\u003e\n      As a Rails developer, you care about user experience and performance,\n      but you also want simple and maintainable code. Achieve all that by\n      embracing the full stack of web development, from styling with\n      Bootstrap, building an interactive user interface with AngularJS, to\n      storing data quickly and reliably in PostgreSQL. Take a holistic view of\n      full-stack development to create usable, high-performing applications,\n      and learn to use these technologies effectively in a Ruby on Rails\n      environment.\n      \u003c/p\u003e","image_url":"dcbang.jpg","price":"45.0","created_at":"2017-06-03T04:40:46.676Z","updated_at":"2017-06-03T04:40:46.676Z","orders":[{"id":1,"name":"Dave Thomas","address":"123 Main St","email":"customer@example.com","pay_type":"Check","created_at":"2017-06-03T04:42:28.783Z","updated_at":"2017-06-03T04:42:28.783Z"},{"id":2,"name":"Dave Thomas","address":"123 Main St","email":"customer@example.com","pay_type":"Check","created_at":"2017-06-03T04:42:29.370Z","updated_at":"2017-06-03T04:42:29.370Z"}]}

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?(@latest_order)
      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="Rails, Angular, Postgres, and Bootstrap">
  <order>
    <name>Dave Thomas</name>
    <email>customer@example.com</email>
  </order>
  <order>
    <name>Dave Thomas</name>
    <email>customer@example.com</email>
  </order>
</order_list>

Verify that the tests still pass

rails test
Run options: --seed 61646
 
# Running:
 
.......................................
 
Finished in 0.838772s, 46.4966 runs/s, 92.9931 assertions/s.
 
39 runs, 78 assertions, 0 failures, 0 errors, 0 skips

Commit

git commit -a -m "Orders"
[master 0b8e19c] Orders
 7 files changed, 83 insertions(+), 43 deletions(-)
git tag iteration-g

14.1 Iteration I1: Email Notifications 12.4 Iteration G2: Downloading an eBook