--- 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> ```