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