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