Añadiendo comentarios a nuestro sitio
Aún debemos agregar los comentarios a nuestro sitio, el proceso es bastante simple, seguiremos los siguientes pasos:
- Generar el modelo.
- Editar los modelos para establecer las relaciones.
- Crear el controlador.
- 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
- Hacemos uso de resourcify para poder trabajar con roles.
- Establecemos la relación con el usuario.
- Establecemos relación con un fact.
- 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:
before_action :find_fact
nos sirve para encontrar el fact al cual deseamos de añadir un comentario.load_and_authorize_resource :comment, :through => :fact
es una validación para nuestros permisos con CanCanCan.@comment = @fact.comments.create(comment_params)
creamos un comentario en base al Fact que deseamos.@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.