---
title: 'Ejercicio guiado - catalog'
tags: Desarrollo de aplicaciones Ruby on Rails, guiado
---
{%hackmd hackmd-dark-theme %}
<style>
.markdown-body {
color: #fff;
}
pre.part {
background: #282A36;
color: #FFB439;
}
.markdown-body h2 {
border-bottom: none;
}
.hljs-keyword {
color: #f5c;
font-weight: bold;
}
.hljs-title {
color: #3f7;
}
h1 {text-align: center}
</style>
# Ejercicio guiado - Catalogo
Referencia:
{%pdf https://mach-911.github.io/assets/desarrollo-aplicaciones-ror/1.presentacion-modelos-con-relaciones-n-a-n.pdf %}
Creamos un nuevo proyecto:
```bash
rails new catalog -d postgresql
```
>Modificamos los datos de conexión
Crear la base de datos:
```bash
rails db:create
```
Identificamos la relación de entidades:
```mermaid
%%{
init: {
'theme': 'base',
'themeVariables': {
'background': '#fff',
'primaryColor': '#FFB439',
'primaryTextColor': '#222',
'primaryBorderColor': '#FFB439',
'tertiaryColor': '#fff'
},
'themeCSS': '.er.attributeBoxEven { fill:#ddd} .er.relationshipLine { stroke:#ccc}'
}
}%%
erDiagram
PRODUCTO }|--|{ CATEGORIA : tiene
PRODUCTO {
int id
string nombre
int precio
string tamano
}
CATEGORIA {
int id
string nombre
}
```
Normalizar las tablas:
```mermaid
%%{
init: {
'theme': 'base',
'themeVariables': {
'background': '#fff',
'primaryColor': '#FFB439',
'primaryTextColor': '#222',
'primaryBorderColor': '#FFB439',
'tertiaryColor': '#fff'
},
'themeCSS': '.er.attributeBoxEven { fill:#ddd} .er.relationshipLine { stroke:#ccc}'
}
}%%
erDiagram
PRODUCTO ||--|{ CATEGORIA_PRODUCTO : esta
CATEGORIA ||--|{ CATEGORIA_PRODUCTO : esta
CATEGORIA_PRODUCTO {
int producto_id
int categoria_id
}
PRODUCTO {
int id
string nombre
int precio
string tamano
}
CATEGORIA {
int id
string nombre
}
```
Creamos el modelo para Categoria a través de scaffold:
```
rails g scaffold category name
```
Creamos el modelo para Producto a través de scaffold:
```
rails g scaffold product name price:integer size:integer
```
Creamos una migración de representar la tabla intermedia entre categorias y productos:
```
rails g migration CreateJoinTableCategoriesProducts category product
```
>La tabla se creará en la base de datos, pero no establece relaciones por medio de claves foráneas
Realizamos la migración:
```
rails db:migrate
```
Agregamos las relaciones de las categorias a los productos:
```ruby
#app/models/category.rb
class Category < ApplicationRecord
has_and_belongs_to_many :products
end
```
Agregamos las relaciones de productos a las categorías:
```ruby
class Product < ApplicationRecord
has_and_belongs_to_many :categories
end
```
Agregamos [faker](https://github.com/faker-ruby/faker) al proyecto:
```bash
bundle add faker
```
Validamos a través del modelo que la categoría tenga nombre único:
```ruby
#app/models/category.rb
validates :name, uniqueness: true
```
Rellenamos la tabla de categoría con datos de prueba por medio de la siembra:
```ruby
#db/seeds.rb
while Category.count < 10
if !Category.pluck(:name).include?(Faker::Game.genre)
unique_name = Faker::Game.genre
Category.create(name: unique_name)
end
end
```
Podemos correr ahora la migración y ejecutar la siembra:
```
rails db:migrate db:seed
```
Definimos la ruta productos como root de la aplicación:
```ruby=
Rails.application.routes.draw do
resources :products
resources :categories
root "products#index"
end
```
Agregamos el campo `category_ids` a los **strong params** al final del controlador:
```ruby=67
#app/controllers/product_controller.rb
def product_params
params.require(:product)
.permit(:name, :price, :size, :category_ids => [])
end
```
Al crear un producto lo asociamos a una categoría, agreguemos el campo al formulario de producto:
```erb=28
<!-- app/views/products/_form.html.erb -->
<div>
<%= form.label :category_ids, "Categoría", style: "display: block" %>
<%= form.collection_check_boxes(:category_ids, @categories, :id, :name) do |b| %>
<%= b.check_box %><%= b.label %><br>
<% end %>
</div>
```
Ahora para que podamos cargar la información correctamente en el campo anteriormente creado al formulario, añadimos el método para traer las categorias en el controlador de productos:
```ruby=73
def set_categories
@categories = Category.all
end
```
Y lo ponemos a disposición de las acciones del controlador a través de un `before_action`:
```ruby=3
before_action :set_categories, only: [:new, :edit]
```
Ahora tenemos listo para guardar toda esa información correctamente, debemos modificar el método `create` de la siguiente manera para devolver mensajes en español:
```ruby
def create
@product = Product.new(product_params)
respond_to do |format|
if @product.save
format.html { redirect_to product_url(@product), notice: "Producto creado con exito." }
format.json { render :show, status: :created, location: @product }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
```
## Agregando bootstrap con importmap
**1 )** Ejecutar el siguiente comando:
bash:
```bash
bin/importmap pin bootstrap --download
```
cmd:
```cmd
ruby bin/importmap pin bootstrap --download
```
**2 )** En el archivo `config/importmap.rb` remplazar las siguiente líneas:
```ruby=8
pin "bootstrap" # @5.3.0
pin "@popperjs/core", to: "@popperjs--core.js" # @2.11.8
```
Por las siguientes:
```ruby=8
pin "bootstrap", to: "bootstrap.min.js", preload: true
pin "popperjs", to: "popper.js", preload: true
```
**3 )** Modificas las siguiente línea en `config/initializers/assets.rb`:
Descomentar y remplazar:
```ruby=12
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
```
```ruby=12
Rails.application.config.assets.precompile += %w( bootstrap.min.css popper.js )
```
**4 )** Añadir las siguientes líneas en el archivo `app/javascript/application.js`:
```ruby=4
import "popperjs" # primero debe ir popper
import "bootstrap"
```
**5 )** Añadir las siguientes líneas en el archivo `app/config/manifest.js`:
```javascript=5
//= link popper.js
//= link bootstrap.min.js
```
## Agregando usuarios
- Realizar autenticación básica con Devise
- Un usuario puede ir a cerrar sesión, registrarse, desde un navbar de bootstrap
Añadimos devise al proyecto:
```bash
bundle add devise
```
Instalamos el generador:
```
rails g devise:install
```
Generamos la migración:
```bash
rails g devise User
```
Corremos la nueva migración:
```
rails db:migrate
```
Ahora podemos tener un partial con el bloque de código con la navegación y links para el registro e iniciar sesión en `app/views/shared/_navbar.html.erb`:
```erb
<nav class="navbar navbar-expand-lg bg-info bg-gradient">
<div class="container">
<%= link_to("Catalogo", root_path, class: "navbar-brand") %>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<div class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item dropdown">
<%= link_to(user_signed_in??
"Hola #{current_user.email}"
: "Debes registrarte", "#",
class: "nav-link dropdown-toggle",
data: {bs_toggle: "dropdown"},
aria_expanded: false) %>
<div class="dropdown-menu">
<% if user_signed_in? %>
<%= button_to 'Cerrar sesión', destroy_user_session_path, class: 'dropdown-item', method: :delete %>
<% else %>
<%= link_to 'Iniciar sesión', new_user_session_path, class: 'dropdown-item'%>
<hr class="dropdown-divider">
<%= link_to 'Registro', new_user_registration_path, class: 'dropdown-item'%>
<% end %>
</div>
</li>
</div>
</div>
</div>
</nav>
```
Y ahora renderizarlo en el layout principal `app/views/layouts/application.html.erb`:
```erb=
<!DOCTYPE html>
<html>
<head>
<title>Catalogo</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= render 'shared/navbar' %>
<div class="container">
<% if flash[:notice] %>
<div class="alert alert-info"><%= notice %></div>
<% end %>
<%= yield %>
</div>
</body>
</html>
```
---
## Agregando reacciones a los productos
Ahora vamos a permitir que los usuarios puedan permitir a los productos, para eso veamos como quedaría las entidades
```mermaid
%%{
init: {
'theme': 'base',
'themeVariables': {
'background': '#fff',
'primaryColor': '#FFB439',
'primaryTextColor': '#222',
'primaryBorderColor': '#FFB439',
'tertiaryColor': '#fff'
},
'themeCSS': '.er.attributeBoxEven { fill:#ddd} .er.relationshipLine { stroke:#ccc}'
}
}%%
erDiagram
PRODUCTO ||--|{ REACCION : tirne
USUARIO ||--|{ REACCION : realiza
PRODUCTO {
int id
string nombre
int precio
string tamano
}
REACCION {
int product_id
string user_id
}
USUARIO {
int id
string email
string password
}
```
Creamos el modelo para reacciones:
```bash
rails g model Reaction product:references user:references kind
```
Ahora debemos establecer las relaciones para muchos a muchos usando `through`.
Partamos con productos:
```ruby=3
has_many :reactions
has_many :users, through: :reactions
```
Ahora con usuarios:
```ruby=6
has_many :reactions
has_many :products, through: :reactions
```
>Este tipo de relaciones se usa solo cuando creamos un modelo para la tabla intermedia
Ahora en el modelo de reacciones, limitamos el tipo de las mismas:
```ruby=4
validates :kind, acceptance: {
accept: %w[like dislike boring neutral uninsterested interested]
}
```
Agregamos un método de clase para visualizar opciones fácilmente:
```ruby=8
def self.kinds
%w[like dislike boring neutral uninsterested interested]
end
```
Probamos el método de clase a través de la consola `rails c`:
```
Reaction.kinds
```
Generamos el controlador para las reacciones:
```
rails g controller ractions
```
Ahora en el controlador creamos un método para que el usuario pueda reaccionar:
```ruby=
class ReactionsController < AplicattionController
def user_reaction
@user = current_user # capturamos al usuario actual
@product = Product.find(params[:product_id])
reaction = Reaction.find_by(user_id: @user.id, product_id: @product.id)
if reaction
return flash.now[:alert] = "Ya haz reaccionado a este producto"
else
@new_reaction = Reaction.new(user_id: @user.id, product_id: @product.id, kind: params[:kind])
respond_to |format|
if @new_reaction.save!
format.html { redirect_to product_path(@product), notice: "#{current_user.email} a reaccionado con un #{@new_reaction.kind}"}
else
format.html { redirect_to product_path(@product), status: unprocessable_entity}
end
end
end
end
end
```
Ahora agregamos la restricción para que solo un usuario conectado pueda reaccionar:
```ruby=2
before_action :authenticate_user!
```
Creamos la ruta para el método nuevo que creamos:
```
post '/reactions', to: 'reactions#user_reaction', as: 'user_reaction'
```
Agregamos una vista parcial para mostrar las opciones de reacciones:
```erb
#app/views/reactions/_options.html.erb
<div class="container">
<ul>
<% Reaction.kinds.each do |kind| %>
<li><%= button_to "#{kind}", user_reaction_path(product_id: @product.id, kind: kind),
method: :post %></li>
<% end %>
</ul>
</div>
```
Y agregamos otro parcial para mostrar el conteo de reacciones:
```erb
<ul>
<% Reaction.kinds.each do |kind| %>
<li><%= @product.reactions.where(kind: kind).count %> - <%= kind %></li>
<% end %>
</ul>
```
Llamamos a la vista parcial en la vista **show** del producto para renderizarla:
```erb
<p style="color: green"><%= notice %></p>
<%= render @product %>
<%= render 'reactions/counter' %>
<%= render 'reactions/options' %>
<div>
<%= link_to "Edit this product", edit_product_path(@product) %> |
<%= link_to "Back to products", products_path %>
<%= button_to "Destroy this product", @product, method: :delete %>
</div>
```
Creamos ahora un método para que el usuario vea los productos a los cuales ha reaccionado:
```ruby
#app/controllers/reactions_controller.rb
def product_with_reactions
@reactions = current_user.reactions
product_ids = @reactions.map(&:product_id)
@products = Product.where(id: product_ids)
end
```
Creamos una ruta para nuestro nuevo método:
```ruby
#config/routes.rb
get '/my_reactions', to: 'reactions#product_with_reactions', as: 'my_reactions'
```
Para que nuestro método funcione, debemos crear la vista:
```erb
<h1>Products</h1>
<ul>
<% @products.each do |product| %>
<li>
<%= product.name %> -
<%= product.reactions.find_by(user_id: current_user.id).kind %>
<p><%= link_to "Show this product", product %></p>
</li>
<% end %>
</ul>
```