AWD 12: Tarea G: Check Out - LPC-Ltda/Ruby-on-Rails GitHub Wiki

##12.1 Iteración G1: Capturando una orden

Primero creamos el modelo order y actualizamos la table line_items.

$ rails generate scaffold Order name address:text email pay_type
$ rails generate migration add_order_to_line_item order:references

$ rake db:migrate

El tipo de datos por defecto es string.

Creando el formulario de captura de la orden

Agregamos el botón Checkout

[rails40/depot_o/app/views/carts/_cart.html.erb]

<h2>Your Cart</h2>
<table>
  <%= render(cart.line_items) %>

  <tr class="total_line">
    <td colspan="2">Total</td>
    <td class="total_cell"><%= number_to_currency(cart.total_price) %></td>
  </tr>
</table>

<%= button_to "Checkout", new_order_path, method: :get %>
<%= button_to 'Empty cart', cart, method: :delete, data: { confirm: 'Are you sure?' }

Lo primero que haremos es asegurarnos que haya algo en el carro. Esto requiere que tengamos acceso al carro. También necesitamos esto cuando creamos una orden.

[rails40/depot_o/app/controllers/orders_controller.rb]

class OrderController < ApplicationController
  inlcude CurrentCart
  before_action :set_cart, only: [:new, :create]
  before_action :set_order, only: [:show, :edit, :update, :destroy]

  # GET /orders
  #---
end

[rails40/depot_o/app/controllers/orders_controller.rb]

def new
  if @cart.line_items.empty?
    redirect_to store_url, notice: "Your cart is empty"
    return
  end

  @order = Order.new
end

Y agregamos un test

[rails40/depot_o/test/controllers/orders_controller_test.rb]

test "requires item in cart" do
  get :new
  assert_redirected_to store_path
  assert_equal flash[:notice], 'Your cart is empty'
end

test "should get new" do
  item = LineItem.new
  item.build_cart
  item.product = products(:ruby)
  item.save!
  session[:cart_id] = item.cart.id
  get :new
  assert_response :success
end

[rails40/depot_o/app/views/orders/new.html.erb]

<div class="depot_form">
  <fieldset>
    <legend>Please Enter Your Details</legend>
    <%= render 'form' %>
  </fieldset>
</div>

[rails40/depot_o/app/views/orders/_form.html.erb]

<%= form_for(@order) do |f| %>
  <% if @order.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@order.errors.count, "error") %>
      prohibited this order from being saved!</h2>
      <ul>
      <% @order.errors.fill_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name, size: 40 %>
  </div>
  <div class="field">
    <%= f.label :address %><br>
    <%= f.text_area :address, rows: 3, cols: 40 %>
  </div>
  <div class="field">
    <%= f.label :email %><br>
    <%= f.email_field :email, size: 40 %>
  </div>
  <div class="field">
    <%= f.label :pay_type %><br>
    <%= f.select :pay_type, Order::PAYMENT_TYPES, prompt: 'Select a payment method' %>
  </div>
  <div class="actions">
    <%= f.submit 'Place Order' %>
  </div>
<% end %>

[rails40/depot_o/app/models/order.rb]

class Order < ActiveRecord::Base
  PAYMENT_TYPES = [ "Check", "Credit Card", "Purchase order" ]
end

Y un poco de CSS

[rails40/depot_o/app/assets/stylesheets/application.css.scss]

.depot_form {
  fieldset {
    background: #efe;

    legend {
      color: #dfd;
      background: #141;
      font-family: sans-serif;
      padding: 0.2em 1em;
    }
  }

  form {
    label {
      width: 5em;
      float: left;
      text-align: right;
      padding-top: 0.2em;
      margin-right: 0.1em;
      display: block;
    }

    select, textarea, input {
      margin-left: 0.5em;
    }

    .submit {
      margin-left: 4em;
    }

    br {
      display: none
    }
  }
}

Ahora agregemos algunas validaciones

[rails40/depot_o/app/models/order.rb]

class Order < ActiveRecord::Base
  # ...
  validates :name, :address, :email, presence: true
  validates :pay_type, inclusion: PAYMENT_TYPES
end

Ya que modificamos las reglas de validación necesitamos mofificar las fixtures de pruebas con las que hay que coincidir.

[rails40/depot_o/test/fixtures/orders.yml]

# Read about fixtures at
# http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  name: Dave Thomas
  address: MyText
  email: [email protected]
  pay_type: Check
two:
  name: MyString
  address: MyText
  email: MyString
  pay_type: MyString

[rails40/depot_o/test/fixtures/line_items.yml]

# Read about fixtures at
# http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  product: ruby
  order: one

two:
  product: ruby
  cart: one

Capturando el detalle de la orden

[rails/depot_o/app/models/line_item.rb]

class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :product
  belongs_to :cart
  def total_price
    product.price * quantity
  end
end

[rails40/depot_o/app/models/order.rb]

class Order < ActiveRecord::Base
  has_many :line_items, dependent: :destroy
  # ...
end

[rails40/depot_o/app/controllers/order_controller.rb]

def create
  @order = Order.new(order_params)
  @order.add_line_items_form_cart(@cart)

  respond_to do |format|
    if @order.save
      Cart.destroy(session[:cart_id])
      session[:cart_id] = nil

      format.html { redirect_to store_url, notice: 'Thank you for your oreder.' }
      format.json { render action: 'show', status: :created, location: @order }
    else
      format.html { render action: 'new' }
      format.json { render json: @order.errors, status: :unprocessable_entity }
    end
  end
end

[rails40/depot_p/app/models/order.rb]

class Order < ActiveRecord::Base
  #...
  def add_line_items_from_cart(cart)
    cart.line_items.each do |item|
      item.cart_id = nil
      line_items << item
    end
  end
end

Necesitamos modificar el test para reflejar la nueva redirección.

[rails40/depot_p/test/controllers/orders_controller_test.rb]

test "should create order" do
  assert_difference('Order.count') do
    post :create, order: { address: @order.address, email: @order.email, name: @order.name, pay_type: @order.pay_type }
  end
  
  assert_redirected_to store_path
end

Un último cambio Ajax

Como usamos Ajax para refrescas el estado del carro, el mensaje "Thank you for your order" puede permancer desplegado aun si volvemos a comprar. Afortunadamente se resuelve facilmente: sólo ocultamos el <div> que contiene el mensaje flash cuando agregamos algo a la carta.

[rails40/depot_p/app/views/line_items/create.js.erb]

$('#notice').hide();

if ($('#cart tr').lenght == 1) { $('#cart').show('blind', 1000); }

$('#cart').html("<%= escape_javascript render(@cart) %>");

$('#current_item').css({'background-color':'#88ff88'}).animate({'background-color':'#114411'}, 1000);

##12.2 Iteración G2: Atom Feeds

Usar un formato de feed estándar, como Atom, significa que usted puede tomar ventaja inmediatamente de una amplia variedad de clientes pre-existentes. Ya que Rails ya conoce IDs, fechas y links, esto lo libera de tener que preocuparse por estos detalles y se puede concentrar en un resumen legible para el humano. Comenzamos por agregar una nueva acción al controlados de productos.

[rails40/depot_p/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.atom
    end
  end
end

Adicionalmente a obtener el producto, verificamos si el requerimiento es antiguo. Recuerde en la sección 8.5, Iteración C5: Cahing of partial results, cuando ponemos en cache los resultados parciales de las respuestas debido a que el despleigue del catálogo es una área de alto tráfico. Bien, los feeds son como eso, pero con un patrón de uso diferente. En lugar de un gran número de clientes diferentes todos requiriendo la misma página, tenemos un pequeño número de clientes solicitando repetidamente la mísma página, si usted está familiarizado con la idea de los caches de browsers, entonces las mismas cosas aplican para los aggregators de feed.

La forma en que esto funciona es que la respuesta contiene un trozo de metadata que identifica cuando el contenido fue finalmente modificado y un valor en hash llamado ETag. Si un requerimiento posterior entrega esta data como respuesta, esto le da al servidor la oportunidad de responder con un cuerpo de respuesta vacío y una indicación de que la data no ha sido modificada.

Como es usual con Rails, usted no necesita preocuparse por los mecanismos. Usted sólo necesita identificar la fuente del contenido, y Rails hará el resto. En este caso usamos la última orden. Dentro del if procesamos el requerimiento normalmente.

Al agregar format.atom, causamos que Rails busque un template llamado who_bought.atom.builder. Como template puede usar la funcionalidad XML genérica que Builder provee así como usar el conocimiento del formato de feed Atom que el helper atom_feed provee.

[rails40/depot_p/app/views/products/who_bought.atom.buider]

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

En el nivel general del feed, necesitamos proveer sólo dos piezas de información: el título y la última fecha de actualización. Si no hay órdenes, el valor updated_at será null, y Rails proporcionará la hora actual a cambio.

Luego iteramos sobre cada orden asociada con ese producto. Note que no hay una relación directa entre estos dos modelos. De hecho, la relación es indirecta. Los productos tienen muchas line_items, y line_item pertenece a una orden.

Podemos iterar a través de ellas, pero al declarar que hay una relación entre productos y órdenes a tavés de line_items, podemos simplificar el código.

[rails40/depot_p/app/models/product.rb]

class Product < ActiveRecord::Base
  has_many :line_items
  has_many :orders, throught: :line_items
  #...
end

Para cada orden, proveemos un título, un resumen, y un autor. El resumen puede ser completamente XHTML, y usamos esto para producir una tabla de títulos de productos, cantidades ordenadas y precios totales. A continuación de esta tabla un párrafo que contiene el pay_type.

Para que esto funcione necesitamos una ruta.

[rails40/depot_p/config/routes.rb]

Depot::Application.routes.draw do
  resources :orders
  resources :line_items
  resources :carts

  get "store/index"
  resources :products do
    get :who_bought, on: :member
  end

  # The priority is based upon order of creation:
  # first created -> highest priority.
  # See how all your routes lay out with "rake routes".
  # You can have the root of your site routed with "root"
  root 'store#index", as: 'store'
  #...
end

Usted lo puede probar por usted mismo:

$ curl --silent http://localhost:3000/products/3/who_bought.atom
⚠️ **GitHub.com Fallback** ⚠️