Añadiendo comentarios a nuestro sitio

Aún debemos agregar los comentarios a nuestro sitio, el proceso es bastante simple, seguiremos los siguientes pasos:

  1. Generar el modelo.
  2. Editar los modelos para establecer las relaciones.
  3. Crear el controlador.
  4. Crear las vistas.

Generando el modelo

Para generar el modelo haremos uso del comando rails g model comment content:text user:references fact:references con este crearemos una tabla comment con una columna content y las relaciones con el fact y el usuario al cual se refieren (user_id, fact_id).

Abre el modelo que acabas de generar (app/models/comment.rb) y edita su contenido:

class Comment < ApplicationRecord
  resourcify
  belongs_to :user
  belongs_to :fact
  validates :content, presence: true
end
  1. Hacemos uso de resourcify para poder trabajar con roles.
  2. Establecemos la relación con el usuario.
  3. Establecemos relación con un fact.
  4. Validamos la presencia de un contenido.

Edición de los demás modelos

Ahora, debemos editar los demás modelos para poder que funcione correctamente, comenzaremos con editar nuestro archivo app/models/ability.rb ya que como mencionamos anteriormente, tendremos roles que podrán editar y eliminar comentarios, tal cual hicimos con los Facts.

class Ability
  include CanCan::Ability

  def initialize(user)
    if user.has_role? :admin
      can :manage, :all
    else
      can :read, :all
      can :create, :all
    end

    can :update, Fact do |fact|
      fact.user == user
    end

    can :destroy, Fact do |fact|
      fact.user == user
    end

    # Nuevo código
    can :update, Comment do |comment|
      comment.user == user
    end

    can :destroy, Comment do |comment|
      comment.user == user
    end

  end
end

Ahora, editaremos nuestro modelo de usuario app/models/user.rb para establecer la relación.

class User < ApplicationRecord
  rolify

  has_many :facts, dependent: :destroy
  # Nuevo código
  has_many :comments, dependent: :destroy
  # Nombre del usuario
  validates :name, presence: true, uniqueness: true

  # Paperclip validations
  has_attached_file :avatar, :styles => { :medium => "300x300>", :thumb => "100x100>" }, :default_url => "/images/:style/default_avatar.png"
  validates_attachment_content_type :avatar, :content_type => /\Aimage\/.*\Z/
  validates_with AttachmentSizeValidator, attributes: :avatar, less_than: 1.megabytes

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

Si leíste los comentarios en el código, puedes ver que ha sido cambiado, como puedes haber notado, un usuario tiene muchos comentarios relacionados y de ser eliminado el usuario, todos sus comentarios también serán eliminados.

De igual manera hay una línea en la que verificamos que el nombre del usuario sea único, aunque aún no añadimos esta columna a nuestra tabla de usuarios. Procedamos generando una migración con rails g migration add_name_to_users name:string

Este debería generar una migración que se ve más o menos así:

class AddNameToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :name, :string, unique: true
  end
end

Corre las migraciones con rails db:migrate.

Debemos permitir al usuario actualizar su nombre, por lo tanto en nuestro archivo app/controllers/application_controller.rb debemos permitir :name en el mismo sitio que permitimos los demás datos del usuario, quedando entonces así:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_filter :configure_permitted_parameters, if: :devise_controller?

  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_path, :alert => exception.message
  end

  protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:name, :email, :password) }
      devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:name, :email, :password, :current_password, :avatar) }
    end
end

Finalmente, actualizamos nuestras vista de registro (app/views/users/registrations/edit.html.erb) y (app/views/users/registrations/new.html.erb) para incluir el nuevo campo, finalmente se deberían de ver así respectivamente:

<div class="ui container">
  <h2>Edit <%= resource_name.to_s.humanize %></h2>

  <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, :class => "ui form attached fluid segment" }) do |f| %>
    <%= devise_error_messages! %>

    <div class="field">
      <%= f.label :avatar, class: "ui teal button" %>
      <%= f.file_field :avatar, style: "display: none" %>
    </div>

    <div class="field">
      <%= f.label :name %>
      <%= f.text_field :name, autofocus: true %>
    </div>

    <div class="field">
      <%= f.label :email %>
      <%= f.email_field :email %>
    </div>

    <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
      <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
    <% end %>

    <div class="field">
      <%= f.label :password %> <i>(leave blank if you don't want to change it)</i>
      <%= f.password_field :password, autocomplete: "off" %>
    </div>

    <div class="field">
      <%= f.label :password_confirmation %>
      <%= f.password_field :password_confirmation, autocomplete: "off" %>
    </div>

    <div class="field">
      <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i>
      <%= f.password_field :current_password, autocomplete: "off" %>
    </div>

    <div class="actions">
      <%= f.submit "Update", class: "ui green submit button" %>
    </div>
  <% end %>

  <h3>Cancel my account</h3>

  <p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), class: "ui red button", data: { confirm: "Are you sure?" }, method: :delete %></p>

</div>
<div class="ui container">
  <div class="ui attached message">
    <div class="header">
      Join the crowd!
    </div>
    <p>Fill out the form below to sign-up for a new account</p>
  </div>

  <%= form_for(resource, as: resource_name, url: registration_path(resource_name), :html => {:class => "ui form attached fluid segment"}) do |f| %>
    <%= devise_error_messages! %>

    <div class="field">
      <%= f.label :name %>
      <div class="ui left icon input">
        <%= f.text_field :name %>
        <i class="user icon"></i>
      </div>
    </div>

    <div class="field">
      <%= f.label :email %>
      <div class="ui left icon input">
        <%= f.email_field :email %>
        <i class="globe icon"></i>
      </div>
    </div>

    <div class="field">
      <%= f.label :password %>
      <% if @minimum_password_length %>
      <em>(<%= @minimum_password_length %> characters minimum)</em>
      <% end %>
      <div class="ui left icon input">
        <%= f.password_field :password, autocomplete: "off" %>
        <i class="lock icon"></i>
      </div>
    </div>

    <div class="field">
      <%= f.label :password_confirmation %>
      <div class="ui left icon input">
        <%= f.password_field :password_confirmation, autocomplete: "off" %>
        <i class="lock icon"></i>
      </div>
    </div>

    <div class="actions">
      <%= f.submit "Sign up", class: "ui blue submit button" %>
    </div>

    <p>Once you click on Sign up, you automatically agree to our <a href="#">Terms and Conditions</a>.</p>

  <% end %>

  <div class="ui bottom attached warning message">
    <i class="icon help"></i>
    Already signed up? <%= link_to "Login here", new_user_session_path %> instead.
  </div>

</div>

Debemos de actualizar nuestro modelo fact de igual manera, por tanto abre app/models/fact.rb.

class Fact < ApplicationRecord
  resourcify
  acts_as_votable
  has_attached_file :image, :styles => { :large => "1024x768", :medium => "300x300>", :thumb => "100x100>" }, :default_url => "/images/:style/default_image.png"
  validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/
  validates :image, presence: true
  belongs_to :user
  # Nuevo código
  has_many :comments, dependent: :destroy
end

Como puedes observar hacemos lo mismo que con el usuario, un fact puede tener muchos comentarios y de ser eliminado, todos sus comentarios relacionados también serán destruidos.

Creación del controlador

Generaremos nuestro controlador usando rails g controller comments y abre el archivo generado (app/controllers/comment_controller.rb)

Editalo para que se vea tal cuál:

class CommentsController < ApplicationController
  before_action :authenticate_user!, only: [:edit, :update, :destroy]
  before_action :find_fact
  before_action :find_comment, only: [:edit, :update, :destroy]
  load_and_authorize_resource :fact
  load_and_authorize_resource :comment, :through => :fact

  def create
    @comment = @fact.comments.create(comment_params) 
    @comment.user = current_user

    if @comment.save
      redirect_to fact_path(@fact)
    else
      render 'new'
    end
  end

  def edit
  end

  def update
    if @comment.update(comment_params)
      redirect_to fact_path(@fact)
    else
      render 'edit'
    end
  end

  def destroy
    @comment.destroy
    redirect_to fact_path(@fact)        
  end

  private

    def find_fact
      @fact = Fact.find(params[:fact_id])
    end

    def find_comment
      @comment = @fact.comments.find(params[:id])
    end

    def comment_params
      params.require(:comment).permit(:content, :user_id, :fact_id)
    end
end

La mayoría de lineas las hemos explicado en artículos previos, sin embargo hay unas que requieren atención especial:

  1. before_action :find_fact nos sirve para encontrar el fact al cual deseamos de añadir un comentario.
  2. load_and_authorize_resource :comment, :through => :fact es una validación para nuestros permisos con CanCanCan.
  3. @comment = @fact.comments.create(comment_params) creamos un comentario en base al Fact que deseamos.
  4. @comment.user = current_user asignamos el usuario actual como el creador del Fact.

Debemos también editar nuestras rutas, por tanto abre el archivo config/routes.rb y añade un recurso anidado, de tal manera que queda así:

Rails.application.routes.draw do
  devise_for :users
  root "facts#index"
  resources :facts do
    resources :comments
  end
end

Modificando las vistas

Esto es más de lo mismo, en app/views/comments/ crea los siguientes archivos:

  • _comment.html.erb
  • _form.html.erb
  • _edit_form.html.erb
  • edit.html.erb

Nuestro archivo _comment.html.erb debería quedar así:

<div class="comment">
  <a class="avatar">
    <%= image_tag comment.user.avatar.url(:thumb) %>
  </a>
  <div class="content">
    <a class="author"><%= comment.user.name %></a>
    <div class="metadata">
      <div class="date"><%= comment.created_at.strftime("%b %d %Y, %H:%M") %></div>
      <div class="rating">
        <i class="star icon"></i>
        5 Likes
      </div>
    </div>
    <div class="text">
      <%= comment.content %>
    </div>
    <% if current_user %>
      <div class="actions">
        <% if can? :update, comment %>
          <%= link_to "Edit", edit_fact_comment_path(comment.fact, comment) %>
        <% end %>
        <% if can? :destroy, comment %>
          <%= link_to "Delete", fact_comment_path(comment.fact, comment), method: :delete, data: { confirm: "Are you sure?" } %>
        <% end %>
      </div>
    <% end %>
  </div>
</div>

El archivo _form.html.erb quedaría así:

<%= form_for ([@fact, @fact.comments.build]), :html => {:class => "ui form attached fluid segment"} do |f| %>

  <div class="field">
    <%= f.text_field :content, style: "min-height:100px;" %>
  </div>

  <div class="actions">
    <%= f.submit "Comment", class: "ui blue submit button" %>
  </div>

<% end %>

En este archivo creamos un comentario en base a un fact, tal cual declaramos en nuestro controlador.

Luego, edita el archivo _form_edit.html.erb:

<%= form_for ([@fact, @comment]), :html => {:class => "ui form attached fluid segment"} do |f| %>

  <div class="field">
    <%= f.label :content %>
    <%= f.text_field :content %>
  </div>

  <div class="actions">
    <%= f.submit "Send comment", class: "ui blue submit button" %>
  </div>

<% end %>

Finalmente edita el archivo edit.html.erb

<div class="ui container">
  <div class="ui attached message">
    <div class="header">
      Editing your comment
    </div>
    <p>Fill out the form in order to update your comment.</p>
  </div>

  <%= render "edit_form" %>

</div>

Ahora, solo resta mostrar los comentarios de cada fact, para esto, edita el archivo app/views/facts/show.html.erb y añade lo siguiente justo antes de la última etiqueta </div>

<h3>Comments</h3>
  <div class="ui comments">
    <%= render @fact.comments %>
  </div>

  <% if current_user %>
    <div class="form">
      <%= render 'comments/form' %>
    </div>
<% end %>

Eso es todo por este artículo, en el próximo y último añadiremos los likes.