Rails-5
Rails-5
El carrito de la compra
El carrito de la compra
Bibliografía:
Agile Web Development with Rails, 3rd ed.
(capítulo 8)
El carrito de la compra - 2
Objetivos
Objetivos
Introducir el concepto de sesión
Aprender a gestionar sesiones en Rails
Usar modelos no soportados por una Base de
Datos
Mejorar la robustez de la aplicación
−
Generar avisos mediante el flash
−Generar eventos de logging
El carrito de la compra - 3 ©GSyC-2009
Sesiones en HTTP
Sesiones en HTTP
Cada usuario navega por el catálogo de productos
Va añadiendo al carrito los productos que desea
Al terminar la
sesión, paga los productos del carrito
Por tanto, la aplicación tiene que recordar los contenidos
que el usuario fue añadiendo al carrito durante la sesión
Problema
: HTTP es un protocolo sin estado (
stateless)
− No implementa el concepto de sesión: no hay relación entre
las diferentes peticiones que envía un navegador: el servidor no relaciona las peticiones
Solución
: hay que implementar en el servidor las
sesiones, para que cuando llegue una nueva petición se
pueda relacionar con el estado que la aplicación
guardapara esa sesión
Mecanismos para implementar sesiones:
− Reescritura de URLs: el identificador de sesión se almacena en la propia sesión
− Uso de galletitas (cookies)
Sesiones mediante
Sesiones mediante
cookies
cookies
Rails implementa las sesiones mediante cookies
− Por tanto ¡los navegadores tienen que tener habilitado el
uso de las cookies!
Una cookie es estado con nombre que intercambian navegador
y servidor
− El servidor manda cookies en las cabeceras de los mensajes
de respuesta que envía al navegador: nombre-cookie:
valor-cookie
− El navegador guarda las cookies en disco
− Cuando el navegador vuelve a visitar un sitio web, envía en
las cabeceras de sus mensajes las cookies que le envió ese sitio web
La aplicación puede utilizar una cookie para implementar las
sesiones:
− En función del valor de una cookie, puede relacionar varias peticiones
El carrito de la compra - 5 ©GSyC-2009
El hash
El hash
session
session
Rails proporciona la funcionalidad de las sesiones mediante
una estructura de tipo hash denominada session, disponible para cada usuario
− Las parejas clave/valor almacenadas en una acción en
dicha hash están disponibles para ser consultadas en acciones posteriores invocadas por el mismo navegador
Implementaremos el carrito de la compra usando el hash session
Por omisión, Rails almacena los contenidos del hash
session en cabeceras set-cookie que envía al navegador
− Problema: ¿qué ocurre si en esos datos hay información
confidencial o sensible...?
− Solución: Rails permite almacenar la sesión en una tabla
de la BD de la aplicación, y enviar como cookie sólo un identificador para acceder a dicha tabla
El carrito de la compra - 6
Para almacenar la sesión en primer lugar creamos una migración que contenga la definición de la tabla de sesiones:
Aplicamos la migración para añadir la tabla sessions al esquema de nuestra BD:
Configuramos Rails para almacene la sesión en la BD en vez de almacenar todo en cookies:
− En config/environment.rb descomentamos la última de estas
3 líneas:
Descomentamos el secreto de app/controllers/application.rb
para dificultar la generación de peticiones falsificadas:
Por último rearrancamos el servidor (ruby script/server)
Almacenamiento de sesiones en la
Almacenamiento de sesiones en la
BD
BD
depot> rake db:sessions:create exists db/migrate
create db/migrate/20080601000004_create_sessions.rb
depot> rake db:migrate
# Use the database for sessions instead of the file system # (create the session table with 'rake db:sessions:create')
config.action_controller.session_store = :active_record_store
El carrito de la compra - 7 ©GSyC-2009
Relación entre el carrito y las
Relación entre el carrito y las
sesiones
sesiones
La primera vez que llegue una petición de un navegador
añadiremos un nuevo objeto
cart
a la sesión, con
clave
:cart
(suponemos que existe una clase
Cart
)
Cuando llegen sucesivas peticiones de la misma sesión,
recuperamos el objeto
cart
de la sesión
Añadimos un método privado al controlador
app/controllers/store_controller.rb
¿Por qué privado?
−
porque NO queremos que sea una acción del
controlador
private
def find_cart
session[:cart] ||= Cart.new
end
Creación del carrito
Creación del carrito
El carrito contiene datos e implementa parte de la lógica de la aplicación; por lo tanto ni es una vista ni un controlador: es un modelo
Hasta ahora nuestro único modelo (product.rb) se correspondía con una tabla de la BD. Pero no todos los modelos tienen por qué corresponderse con una tabla de la BD.
− NOTA: El carrito se almacena en la sesión, y la sesión se almacena en
la BD. Pero el carrito no es una tabla de la BD
Ahora no se crea el fichero con la clase del modelo a través de una migración. Al no estar el carrito directamente en la BD
tenemos que crear el modelo escribiendo directamente la clase
Cart en app/models/cart.rb: class Cart attr_reader :items def initialize @items = [] end def add_product(product) @items << product end end
El carrito de la compra - 9 ©GSyC-2009
Relación vista/controlador
Relación vista/controlador
En la vista
app/views/store/index.html.erb
habíamos
añadido a cada producto un botón asociado a la acción
add_to_cart
(acción que aún no hemos escrito)
<%= button_to "Add to Cart", :action => :add_to_cart, :id => product.id %>
− A veces se escribe product en vez de product.id. Es lo
mismo: en este ámbito product es una abreviatura de
product.id
product.id es el campo que usa Rails para identificar un
objeto del modelo. En el caso del producto corresponde con la columna id del producto en la BD (la clave primaria)
La vista le comunica así al controlador qué producto es el
que se quiere añadir
− La acción add_to_cart sabrá así exactamente qué añadir
al carrito
El carrito de la compra - 10
Relación vista/controlador
Relación vista/controlador
Escribimos la acción
add_to_cart
del controlador
app/
controllers/store_controller.rb
− Ojo, tiene que ser public porque es una acción:
def add_to_cart @cart = find_cart
product = Product.find(params[:id]) @cart.add_product(product)
end
− find_cart es el método privado que ya habíamos escrito − params almacena los parámetros pasados por el
navegador a la acción
Es un convenio que el parámetro :id sea el id (clave primaria si está en la BD) del objeto que tiene que utilizar la acción
params[:id] es como se recibe el :id => product.id especificado en el botón que aparece en la vista junto a cada producto
La url que se invoca al pulsar el botón es esta:
− http ://localhost:3000/store/add_to_cart/XX
El carrito de la compra - 11 ©GSyC-2009
Relación vista/controlador
Relación vista/controlador
Si probamos pulsando el botón de un producto aún no
funciona:
Tenemos que añadir una vista para esta acción:
app/views/store/add_to_cart.html.erb
<h2>Your Pragmatic Cart</h2> <ul>
<% for item in @cart.items %> <li><%= h(item.title) %></li>
<% end %>
</ul>
Ahora sí: si recargamos en el navegador (reenviando
por tanto la acción del botón para el producto 2)
vemos el carrito:
Si vamos de nuevo a la página principal y añadimos nuevos productos los veremos en esta vista
Mejora del carrito
Mejora del carrito
Si un usuario compra 10
unidades de un mismo
producto en la vista
aparecen 10 líneas
− Mejora: que salga una sóla línea de ese
producto, indicando que quiere 10 unidades del mismo
Lo que haremos es
guardar cada producto
una sóla vez en el
carrito, junto a la
cantidad
Creamos para ello un
nuevo modelo, la clase
CartItem
app/models/cart_item.rb
class CartItem attr_reader :product,:quantity def initialize(product) @product = product @quantity = 1 end def increment_quantity @quantity += 1 end def title @product.title end def price @product.price * @quantity end endEl carrito de la compra - 13 ©GSyC-2009
Mejora del carrito
Mejora del carrito
app/models/cart.rb
def add_product(product)current_item = @items.find {|item| item.product == product} if current_item current_item.increment_quantity else @items << CartItem.new(product) end end
Ahora tenemos que modificar
Cart#add_product
Y a continuación modificamos la vista de
add_to_cart
para que extraiga la información del carrito mejorado
app/views/store/add_to_cart.html.erb
<h1>Your Pragmatic Cart</h1> <ul>
<% for cart_item in @cart.items %>
<li><%= cart_item.quantity %> × <%= cart_item.title) %></li>
<% end %>
</ul>
El carrito de la compra - 14
Mejora del carrito
Mejora del carrito
Probamos la aplicación y… no funciona:
El problema es que la sesión está almacenada en la
BD y los items del carrito son objetos
Product ¡pero add_product, llamado desde add_to_cart
, espera
El carrito de la compra - 15 ©GSyC-2009
Mejora del carrito
Mejora del carrito
Solución: limpiar la tabla de sesiones en la BD...
depot>
rake db:sessions:clear
y comenzar una nueva sesión desde la página del
catálogo pulsando Atrás y Recargar en el navegador.
Si no se hace así, aparecerá un error de ActionController::InvalidAuthenticityToken
al tratar de actuar sobre una sesión que ya no existe)
Mejora del carrito
Mejora del carrito
Problema: si la aplicación estaba ya en producción,
habríamos vaciado las sesiones, y por tanto los
carritos, de todos nuestros clientes
Solución: Sería mejor no guardar información del
modelo en la sesión. Así si lo cambiamos no hay que
inicializar las sesiones
− Para hacerlo podríamos hacer (aunque no lo haremos en
esta aplicación):
que el modelo Cart fuese un modelo Active Record (es
decir, respaldado por una tabla en la BD)
almacenar en la sesión simplemente el id de un Cart que
está en la BD
Cuando llegue la petición se extraería el id del carrito de
la sesión y a continuación recuperaríamos el carrito de la BD
El carrito de la compra - 17 ©GSyC-2009
Mejora de la robustez:
Mejora de la robustez:
flash, log
flash, log
Si se envían peticiones mal formadas la
aplicación puede mostrar errores que pueden
dar lugar a fallos de seguridad, y además no
queda bien que la aplicación “se rompa”:
Al pedirle un id de producto inexistente, se eleva una excepción en la acción add_to_cart del controlador Podríamos ignorarla, pero entonces no detectaríamos errores El carrito de la compra - 18
Mejora de la robustez:
Mejora de la robustez:
flash, log
flash, log
El flash:− El flash es una estructura de datos tipo hash en la que se
pueden almacenar datos mientras procesamos una petición (se guarda en la sesión)
− Sus contenidos están disponibles en la siguiente petición
de la misma sesión
− Ejemplo de uso:
Si nos piden un id de producto inválido en la acción add_to_cart
almacenaremos una indicación de error en el flash
Luego redirigiremos el navegador a la acción index del catálogo Finalmente, desde la vista de index mostraremos en la pantalla los
contenidos del flash
− La información almacenada en el flash está disponible
El carrito de la compra - 19 ©GSyC-2009
Mejora de la robustez:
Mejora de la robustez:
flash, log
flash, log
Mejoremos la robustez de la acción
add_to_cart
del
controlador
− Cuando se eleve la excepción porque no exista un id de
producto:
Registraremos la incidencia en el log
Guardaremos un mensaje para el usuario en el flash
Redirigiremos el navegador a la página principal del
catálogo para no mostrar el error
def add_to_cart begin product = Product.find(params[:id]) @cart = find_cart @cart.add_product(product) rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}") redirect_to_index("Invalid product") end end
Mejora de la robustez:
Mejora de la robustez:
flash, log
flash, log
Al cargar de nuevo la URL con un
id
de producto
inexistente como
wibble
aparecerá la siguiente línea
en
log/development.log
Parameters: {"action"=>"add_to_cart", "id"=>"wibble", "controller"=>"store"}
Product Load (0.000427) SELECT * FROM products WHERE (products.id = 'wibble') LIMIT 1
Attempt to access invalid product wibble
Redirected to http://localhost:3000/store/index Completed in 0.00522 (191 reqs/sec) . . .
Processing StoreController#index ... : :
Rendering within layouts/store Rendering store/index
private
def redirect_to_index(msg)
flash[:notice] = msg
redirect_to :action => :index
end
El método redirect_to_index no es una acción (por eso
lo hacemos private), sino un método auxiliar del
controlador en el que se almacena el error en el flash bajo la clave :notice, y se redirige a la página principal:
El carrito de la compra - 21 ©GSyC-2009
Mejora de la robustez:
Mejora de la robustez:
flash, log
flash, log
Para que aparezca lo almacenado en el flash hay que modificar la vista index o, mejor aún, el layout del controlador:
app/views/layouts/store.html.erb
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
<html> <head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot" , :media => "all" %>
</head>
<body id="store"> <div id="banner">
<%= image_tag("logo.png" ) %>
<%= @page_title || "Pragmatic Bookshelf" %> </div>
<div id="columns"> <div id="side">
<a href="http://www....">Home</a><br />
<a href="http://www..../faq">Questions</a><br /> <a href="http://www..../news">News</a><br />
<a href="http://www..../contact">Contact</a><br />
</div>
<div id="main">
<% if flash[:notice] -%>
<div id="notice"><%= flash[:notice] %></div>
<% end -%> <%= yield :layout %> </div> </div> </body> </html> El carrito de la compra - 22
Mejora de la robustez
Mejora de la robustez
Y añadimos a la hoja de estilo cómo se mostrará el flash:
public/stylesheets/depot.css
#notice {
border: 2px solid red; padding: 1em;
margin-bottom: 2em;
background-color: #f0f0f0; font: bold smaller sans-serif; }
Finalmente aparece el flash en la página a la que se
redirecciona al navegador cuando se pide una URL con un
El carrito de la compra - 23 ©GSyC-2009
Últimos retoques: vaciar
Últimos retoques: vaciar
carrito
carrito
Para terminar el carrito, vamos a añadir un botón para
vaciar el carrito en la vista que lo muestra
app/views/store/add_to_cart.html.erb
<h1>Your Pragmatic Cart</h1> <ul>
<% for cart_item in @cart.items %>
<li><%= cart_item.quantity %> × <%= h(cart_item.title) %></li>
<% end %>
</ul>
<%= button_to "Empty cart", :action => :empty_cart %>
def empty_cart
session[:cart] = nil
redirect_to_index("Your cart is currently empty”)
end
Y añadimos la acción correspondiente al controlador
que elimina el carrito de la sesión, y luego llama a
redirect_to_index
para apuntar el mensaje en el
flash y redirigir a la página principal al navegador
Últimos retoques: estética
Últimos retoques: estética
Por último, retocamos la vista del carrito cambiando la lista HTML por una tabla : app/views/store/add_to_cart.html.erb
<div class="cart-title">Your Cart</div> <table>
<% for cart_item in @cart.items %> <tr> <td><%= cart_item.quantity %>×</td> <td><%= h(cart_item.title) %></td> <td class="item-price"><%= number_to_currency(cart_item.price) %></td> </tr> <% end %> <tr class="total-line"> <td colspan="2">Total</td> <td class="total-cell"><%= number_to_currency(@cart.total_price) %></td> </tr> </table>
<%= button_to "Empty cart", :action => :empty_cart %>
def total_price
@items.sum { |item| item.price } end
− Añadimos unos retoques en la hoja de estilo
(http://media.pragprog.com/titles/rails3/code/depot_i/public/stylesheets/depot.css)
Añadimos el método auxiliar al modelo del carrito
El carrito de la compra - 25 ©GSyC-2009
Aspecto final
Aspecto final
El carrito de la compra - 26Ejercicios
Ejercicios
Añadir una nueva variable contador a la sesión
para controlar el número de veces que un
usuario ha accedido a la acción index.
Mostrar el contador al principio de la página del
catálogo. Puede venirte bien el helper
pluralize
−
Ejemplo de uso:
<%= pluralize(1, "person") %> but <%= pluralize(2, "person") %>
1 person but 2 people
Inicializar el contador a cero cuando el usuario
añada algo al carrito