Travel Guide

Se crea la aplicación con base de datos postgres:

rails new TravelGuide -d postgresql

Configuramos los parámetros de conexión y creamos la base de datos:

rails db:create

Diagrama entidad relación:

had

had

had

had

had

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

Los usuarios vamos gestionarlos usando Devise:

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:

#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:

#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:

rails g model Country name

Lo siguiente es hacer un scaffold de Articles:

rails g scaffold article title description:text when_went:datetime country:references

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:

rails g model Comment content article:references user:references

Ahora hacemos lo mismo para Reaction:

rails g model Reaction kind reaction_type article:references user:references comment:references

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:

# 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:

#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:

#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:

#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:

#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:

#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):

#config/initializers/devise config.navigational_formats = ['*/*', :html, :turbo_stream]

Añadiendo Faker

Integramos la gema Faker al proyecto:

bundle add faker

Ahora vamos agregar datos de prueba en el archivo db/seeds.rb:

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:

#config/routes.rb
root "articles#index"

Agregando tipos de reacciones

En la guía ahora nos dice agregar los tipos de reacciones como constantes:

#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:

rails g controller reactions

Abrimos el controlador app/controllers/reactions_controller.rb:

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:

#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:


<% 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:

rails g controller comments

Ahora podemos definir el método para crear comentarios:

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:

#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:

<%= 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:

<%= render 'comments/form' %>

Ver los comentarios

Como nos señala la guía, debemos agregar comentarios al controlador show de artículos:

#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:

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

<%= render 'comments/article_comments' %>

Ahora agregamos comentarios y reacciones al seed:

#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:

<p>
  <strong>Cuando fui:</strong>
  <%= article.when_went %>
</p>

Mejoras

Si quieres partir desde lo último puedes clonar este repositorio:

https://github.com/enidev911/travelGuide

Añadimos bootstrap para los estilos vía CDN:

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