--- title: "Travel Guide - Desafío guiado" tags: guiados --- # Travel Guide {%hackmd @themes/orangeheart %} {%pdf https://enidev911.github.io/assets/pdf/fullstack-ror/m-6/guia-de-ejercicios%20_relaciones-n-a-n-en-modelos.pdf?#view=FitH&toolbar=0&navpanes=0 %} <style> body { background: linear-gradient(#ffffff70, #f8f8f990); background-repeat: no-repeat; background-attachment: fixed; } .markdown-body{ background: #fff; box-shadow: 2px 3px 12px #00000010; border: 1px solid #999ccc; } .ui-status-lastchange { display: none; } .ui-infobar__actions { display: none; } .ui-infobar__user-info { display: none; } </style> Se crea la aplicación con base de datos postgres: ```bash rails new TravelGuide -d postgresql ``` Configuramos los parámetros de conexión y creamos la base de datos: ```bash rails db:create ``` Diagrama entidad relación: ```mermaid erDiagram Users { id integer email string password string photo string name string } Reactions { article_id integer user_id integer kind string reaction_type integer comment_id integer } Comments { id integer content text article_id integer user_id integer } Articles { id integer title string when_went string country_id integer } Country { id integer name string } Country ||--|{ Articles :had Articles ||--|{ Reactions :had Articles ||--|{ Comments :had Users ||--|{ Comments :had Users ||--|{ Reactions :had ``` Los usuarios vamos gestionarlos usando Devise: ```bash bundle add devise ``` Corremos el generador de devise: ``` rails g devise:install ``` Vamos a generar las vistas de Devise: ``` rails g devise:views ``` Ahora generamos el modelo de Devise: ``` rails g devise User ``` Añadimos los atributos adicionales mediante una migración: ``` rails g migration AddDetailsToUsers photo name ``` Ahora la guía nos dice que agreguemos los **strong params**: ```ruby #app/controllers/application_controller.erb class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :photo]) devise_parameter_sanitizer.permit(:account_update, keys: [:name, :photo]) end end ``` En la guía el siguiente paso es agregar los campos al formulario de registro: ```erb #app/views/devise/registrations/new.html.erb <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name, autocomplete: "name" %> </div> <div class="field"> <%= f.label :photo %><br /> <%= f.text_field :photo, autocomplete: "photo" %> </div> ``` El siguiente paso que propone la guía es crear el modelo Country: ```bash rails g model Country name ``` Lo siguiente es hacer un scaffold de Articles: ```bash! rails g scaffold article title description:text when_went:datetime country:references ``` :::warning **OJO:** En la guía aparece en el comando la palabra en plural (*articles*), pero debemos usarlo en singular y además el campo description tiene que ser de tipo text ya que es menos limitado que el string por defecto. ::: Ahora podemos seguir con el modelo de Comment: ```bash rails g model Comment content article:references user:references ``` Ahora hacemos lo mismo para Reaction: ```bash! rails g model Reaction kind reaction_type article:references user:references comment:references ``` :::warning **Ojo:** La guía en este punto nos dice que debemos revisar la migración, para asegurarnos de que `article_id` y `comment_id` en lo que es el modelo de Reaction permitan tener valores nulos o sea, es decir; `null: true`. ::: Teniendo en cuenta la consideración anterior, el archivo de migración que crea la tabla Reaction se vería de la siguiente forma: ```ruby= # 202........_create_reactions class CreateReactions < ActiveRecord::Migration[7.0] def change create_table :reactions do |t| t.string :kind t.string :reaction_type t.references :article, null: true, foreign_key: true t.references :user, null: true, foreign_key: true t.references :comment, null: true, foreign_key: true t.timestamps end end end ``` Y ahora ejecutamos la migración con `rails db:migrate`. ## Establecer las relaciones Vamos a validar y añadir las relaciones 'asociaciones' correspondiente entre los modelos. Comenzando con el modelo Article: ```ruby #app/models/article.rb class Article < ApplicationRecord belongs_to :country has_many :comments, dependent: :destroy has_many :reactions, dependent: :destroy has_many :users, through: :reactions #or comments doesn't matter end ``` Lo que tenemos establecido en el modelo es lo siguiente: - `belongs_to :country` - Un artículo pertenece a un País. - `has_many :comments, dependent: :destroy` - Un artículo puede tener muchos comentarios, y con *`dependent: :destroy`* le estamos diciendo que cuando se elimine el artículo, se eliminen sus comentarios asociados. - `has_many :reactions, dependent: :destroy` - Un artículo puede tener muchas reacciones, y como mencione anteriormente *`dependent: :destroy`* configurará la eliminación en cascada. - `has_many :users, through: :reactions` - Un artículo puede tener muchas reacciones a través de los usuarios, podríamos haber usado comentarios en vez de los usuarios conseguimos el mismo resultado. Ahora vamos al modelo de Comentario: ```ruby #app/models/comment.rb class Comment < ApplicationRecord belongs_to :article belongs_to :user has_many :reactions, dependent: :destroy end ``` - `belongs_to :article` - Un comentario le pertenece a un artículo. - `belongs_to :user` - Un comentario le pertenece a un usuario. - `has_many :reactions, dependent: :destroy` - Un comentario puede tener muchas reacciones, y también implementa la eliminación en cascada. Seguimos con el modelo de Country: ```ruby #app/models/country.rb class Country < ApplicationRecord has_many :articles, dependent: :destroy end ``` - `has_many :articles` - En un país se pueden publicar muchos artículos, si eliminamos ese país todos sus artículos asociados también se eliminan. Vamos con el modelo Reaction: ```ruby #app/models/reaction.rb class Reaction < ApplicationRecord belongs_to :article, optional: true belongs_to :user belongs_to :comment, optional: true end ``` - `belongs_to :article, optional: true` - Una reacción pertenece a un artículo con *`optional: true`* le estamos diciendo que no valide la presencia del objeto asociado. De forma predeterminada, esta opción está establecida en `false` por lo que el objeto asociado es obligatorio. - `belongs_to :user` - Una reacción le pertenece a un usuario. - `belongs_to :comment, optional: true` - Una reacción le pertenece a un artículo y como sabemos *`optional: true`* no validará la presencia del objeto asociado. Ahora seguimos con el modelo de User: ```ruby #app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validateable has_many :reactions has_many :comments has_many :articles, through: :reactions end ``` Ahora en la guía nos dice que debemos arreglar el problema de inicio de sesión de Devise con la siguiente línea (podemos decomentarla si vamos a la línea 266): ```ruby=265 #config/initializers/devise config.navigational_formats = ['*/*', :html, :turbo_stream] ``` ## Añadiendo Faker Integramos la gema Faker al proyecto: ```bash bundle add faker ``` Ahora vamos agregar datos de prueba en el archivo **db/seeds.rb**: ```ruby until Country.count == 20 do Country.create(name: Faker::Address.country) if !Country.pluck(:name).include?(Faker::Address.country) end countries = Country.all until Article.count == 100 do Article.create(title: Faker::Book.title, description: Faker::Lorem.paragraph_by_chars(number: 200, supplemental: false), when_went: Faker::Date.between(from: 10.years.ago, to: Date.today), country_id: countries.sample.id) end ``` Ahora el siguiente paso es establecer la ruta raíz con la página de artículos: ```ruby #config/routes.rb root "articles#index" ``` ## Agregando tipos de reacciones {%pdf https://enidev911.github.io/assets/pdf/fullstack-ror/m-6/guia-de-ejercicios%20_relaciones-n-a-n-en-modelos.pdf?#view=FitH&toolbar=0&navpanes=0&page=7 %} En la guía ahora nos dice agregar los tipos de reacciones como constantes: ```ruby! #app/models/article.rb Kinds = %w[like dislike not_interest neutral].freeze KindsSpanish = {"like" => "Me gusta", "dislike" => "No me gusta", "not_interest" => "No me interesa", "neutral" => "Neutral"}.freeze ``` Ahora agregamos el controlador, método y ruta para crear reacciones: ```bash rails g controller reactions ``` Abrimos el controlador **app/controllers/reactions_controller.rb**: ```ruby class ReactionsController < ApplicationController def new_user_reaction @user = current_user @type = params[:reaction_type] @article = Article.find(params[:article_id]) if params[:article_id] @comment = Comment.find(params[:comment_id]) if params[:comment_id] @kind = params[:kind] respond_to do |format| (@type == "comment") ? reaction_comment = Reaction.find_by(user_id: @user, comment_id: @comment.id) : reaction_article = Reaction.find_by(user_id: @user.id, article_id: @article.id) if reaction_article || reaction_comment format.html { redirect_to article_path(@article), notice: "Ya haz reaccionado a este artículo" } else (@type == "article") ? @reaction = Reaction.new(user_id: @user.id, article_id: @article.id, reaction_type: @type, kind: @kind) : @reaction = Reaction.new(user_id: @user.id, comment_id: @comment.id, reaction_type: @type, kind: @kind) if @reaction.save! format.html { redirect_to article_path(@article), notice: "Reaccionaste a este artículo" } else format.html { redirect_to article_path(@article, notice: "Algo salio mal, no se pudo reaccionar") } end end end end end ``` El siguiente paso en la guía es añadir la ruta para reaccionar en **config/routes.rb**: ```ruby #config/routes.rb post '/new_user_reaction', to: 'reactions#new_user_reaction', as: 'new_user_reaction' ``` Ahora agregamos el botón para crear reacciones en la vista **show.html.erb**: ```erb <% Article::Kinds.each do |kind| %> <%= button_to "#{Article::KindsSpanish[kind]}", new_user_reaction_path(article_id: @article.id, reaction_type: "article", kind: kind), method: :post %> <% end %> ``` Ahora generamos el controlador para los comentarios: ```bash rails g controller comments ``` Ahora podemos definir el método para crear comentarios: ```ruby def create @article = Article.find(params[:comment][:article_id]) @comment = Comment.new(comment_params) @comment.user = current_user respond_to do |format| if @comment.save format.html { redirect_to article_path(@article.id), notice: "Comentario creado exitosamente" } else format.html { redirect_to article_path(@article.id), notice: "No se pudo crear comentario" } end end end private def comment_params params.require(:comment).permit(:content, :article_id) end ``` Y ahora en la guía nos dice que debemos añadir lo siguiente: ```ruby! #config/routes.rb resources :comments, only: [:create] ``` Lo siguiente es crear un partial en __app/views/comments/\_form.html.erb__ para el formulario de comentarios: ```erb <%= form_with(model: @comment, local: true) do |f| %> <% if @comment.errors.any? %> <div id="error_explanation"> <h2>Este comentario no se pudo crear por las siguientes razones</h2> <ul> <% @comment.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <%= f.hidden_field :article_id, value: @article.id %> <div class="field"> <%= f.label :content, as: "Contenido" %> <%= f.text_field :content %> </div> <div class="action"> <%= f.submit %> </div> <% end %> ``` Ahora nos vamos a la vista **show** de lo artículos para renderizar el partial: ```erb <%= render 'comments/form' %> ``` --- ## Ver los comentarios {%pdf https://enidev911.github.io/assets/pdf/fullstack-ror/m-6/guia-de-ejercicios%20_relaciones-n-a-n-en-modelos.pdf?#view=FitH&toolbar=0&navpanes=0&page=11 %} Como nos señala la guía, debemos agregar comentarios al controlador show de artículos: ```ruby #app/controllers/articles_controller.rb def show @comment = Comment.new @comments = @article.comments end ``` Ahora creamos un partial en **app/views/comments/\_article_comments.html.erb** del artículo: ```ruby <div> <% @comments.each do |comment| %> <p> <%= image_tag(comment.user.photo) %> <%= comment.content %> </p> <% end %> </div> ``` Ahora lo renderizamos en la vista **show** del artículo: ```erb <%= render 'comments/article_comments' %> ``` Ahora agregamos comentarios y reacciones al seed: ```ruby #db/seeds.rb i = 0 until User.count == 20 do User.create(email: "test_user#{i}@gmail.com", password: "asdfasdf", password_confirmation: "asdfasdf", photo: Faker::Avatar.image, name: Faker::Name.name) i += 1 end articles = Article.all users = User.all until Comment.count == 1000 do Comment.create(content: Faker::Lorem.paragraph_by_chars(number: 200, supplemental: false), article_id: articles.sample.id, user_id: users.sample.id) end r_type = %w[article comment] comments = Comment.all kinds = Article::Kinds until Reaction.count == 1000 do rel_type = r_type.sample if rel_type == "article" Reaction.create(article_id: articles.sample.id, user_id: users.sample.id, kind: kinds.sample, reaction_type: rel_type) else Reaction.create(comment_id: comments.sample.id, user_id: users.sample.id, kind: kinds.sample, reaction_type: rel_type) end end ``` Cambiamos la forma en que se muestra el parámetro `when_went` en un partial en **app/views/articles/\_article.html.erb**: ```erb <p> <strong>Cuando fui:</strong> <%= article.when_went %> </p> ``` --- ## Mejoras Si quieres partir desde lo último puedes clonar este repositorio: <i class="fa fa-github"></i> https://github.com/enidev911/travelGuide Añadimos bootstrap para los estilos vía CDN: ```html <!-- CSS BOOTSTRAP --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" /> <!-- JS BOOTSTRAP --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script> ```