feat(controllers): add CommentsController and index
This commit is contained in:
1 parent
259484c7d3
commit
29f97c2db4
1 file changed
+197
@@ -0,0 +1,197 @@
|
|||||||
|
class CommentsController < ApplicationController
|
||||||
|
before_action :require_authentication, only: %i[create edit update destroy approve reject trash restore]
|
||||||
|
|
||||||
|
# Comments were previously only ever visible nested inside
|
||||||
|
# PostsController#show (whatever fits on that one page). This gives them
|
||||||
|
# their own paginated URL, same etag-over-the-page trick as
|
||||||
|
# PostsController#index: current_page/total_count catch a comment being
|
||||||
|
# added or removed shifting what belongs on this page even when nothing
|
||||||
|
# ON this page changed, the id/updated_at pairs catch edits to any of them.
|
||||||
|
def index
|
||||||
|
post = Post.find_by!(slug: params['post_id'])
|
||||||
|
comments = post.comments.includes(:likes).visible_to(current_user, post.user_id)
|
||||||
|
.order(:created_at, :desc).page(params['page']).per(10)
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
fresh_when(etag: [post.id, post.updated_at, current_user&.id, comments.current_page, comments.total_count,
|
||||||
|
comments.to_a.map { |c| [c.id, c.updated_at] }])
|
||||||
|
render(:index, locals: { post: post, comments: comments })
|
||||||
|
end
|
||||||
|
format.json { json(comments.as_json(only: %i[id author body post_id status])) }
|
||||||
|
end
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('posts.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
post = Post.find_by!(slug: params['post_id'])
|
||||||
|
comment = Comment.new(params.require('comment').permit(:author, :body).to_h
|
||||||
|
.merge('post_id' => post.id, 'user_id' => current_user.id))
|
||||||
|
|
||||||
|
if comment.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash[:notice] = t('comments.created_notice')
|
||||||
|
redirect url_for(:post, slug: post.to_param)
|
||||||
|
end
|
||||||
|
format.json { json(comment.as_json(only: %i[id author body post_id status]), 201) }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash[:notice] = comment.errors.full_messages.join(', ')
|
||||||
|
redirect url_for(:post, slug: post.to_param)
|
||||||
|
end
|
||||||
|
format.json { render_errors(comment) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('posts.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
post = Post.find_by!(slug: params['post_id'])
|
||||||
|
comment = Comment.find(params['id'])
|
||||||
|
authorize(comment, :update?)
|
||||||
|
render(:edit, locals: { post: post, comment: comment, errors: [] })
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('comments.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only the body is editable (author is fixed at creation, same reasoning
|
||||||
|
# as PostsController#update not letting the edit form touch cover/user_id)
|
||||||
|
# so Comment#saved_changes below can only ever report `body`. `lock_version`
|
||||||
|
# rides along the same way it does for Post - the value the edit form was
|
||||||
|
# rendered with, checked against the row's actual current version by
|
||||||
|
# Comment#save (locking_enabled? auto-detects the column added by the
|
||||||
|
# add_lock_version_to_comments migration, no model change needed).
|
||||||
|
def update
|
||||||
|
post = Post.find_by!(slug: params['post_id'])
|
||||||
|
comment = Comment.find(params['id'])
|
||||||
|
authorize(comment)
|
||||||
|
comment.assign(params.require('comment').permit(:body, :lock_version).to_h)
|
||||||
|
|
||||||
|
if comment.save
|
||||||
|
changed_fields = comment.saved_changes.keys - ['updated_at', 'lock_version']
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash[:notice] = changed_fields.empty? ? t('comments.update_noop_notice') : t('comments.updated_notice', fields: changed_fields.join(', '))
|
||||||
|
redirect url_for(:post, slug: post.to_param)
|
||||||
|
end
|
||||||
|
format.json { json(comment.as_json(only: %i[id author body post_id status])) }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
status(422)
|
||||||
|
render(:edit, locals: { post: post, comment: comment, errors: comment.errors.full_messages })
|
||||||
|
end
|
||||||
|
format.json { render_errors(comment) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('comments.not_found'))
|
||||||
|
rescue Prism::Orm::StaleObjectError
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
status(409)
|
||||||
|
render(:edit, locals: { post: post, comment: Comment.find(params['id']), errors: [t('comments.stale_notice')] })
|
||||||
|
end
|
||||||
|
format.json do
|
||||||
|
json({ errors: [jsonapi_error(status: 409, pointer: '/data/attributes/lock_version',
|
||||||
|
title: 'is stale', detail: t('comments.stale_notice'))] }, 409)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
post = Post.find_by!(slug: params['post_id'])
|
||||||
|
comment = Comment.find(params['id'])
|
||||||
|
authorize(comment)
|
||||||
|
comment.destroy
|
||||||
|
broadcast_comment_count(post)
|
||||||
|
redirect url_for(:post, slug: post.to_param)
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('posts.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
# approved!/rejected! come straight from Comment's `enum :status` (see
|
||||||
|
# the model), same "find, authorize like an edit, flip the status" shape
|
||||||
|
# as PostsController#publish/#unpublish. CommentPolicy#approve?/#reject?
|
||||||
|
# check the POST's owner, not the comment's - moderation is the post
|
||||||
|
# author's call, not the commenter's.
|
||||||
|
def approve
|
||||||
|
post = Post.find_by!(slug: params['post_id'])
|
||||||
|
comment = Comment.find(params['id'])
|
||||||
|
authorize(comment)
|
||||||
|
comment.approved!
|
||||||
|
broadcast_comment_count(post)
|
||||||
|
flash[:notice] = t('comments.approved_notice')
|
||||||
|
redirect url_for(:post, slug: post.to_param)
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('comments.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject
|
||||||
|
post = Post.find_by!(slug: params['post_id'])
|
||||||
|
comment = Comment.find(params['id'])
|
||||||
|
authorize(comment)
|
||||||
|
comment.rejected!
|
||||||
|
broadcast_comment_count(post)
|
||||||
|
flash[:notice] = t('comments.rejected_notice')
|
||||||
|
redirect url_for(:post, slug: post.to_param)
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('comments.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Every comment a signed-in user has removed, across every post they ever
|
||||||
|
# commented on - a flat, top-level page (see the routes in app.rb), not
|
||||||
|
# nested under any one post, since a commenter's trash is not the same
|
||||||
|
# thing as PostsController#trash and does not depend on whether the post
|
||||||
|
# itself is trashed (Comment#acts_as_paranoid keeps its own deleted_at,
|
||||||
|
# independent of its post's - see the model). Comment.only_deleted (see
|
||||||
|
# acts_as_paranoid) is the one place in this controller that deliberately
|
||||||
|
# reaches past the default paranoid scope every other action relies on to
|
||||||
|
# hide soft-deleted rows.
|
||||||
|
def trash
|
||||||
|
comments = Comment.only_deleted.where(user_id: current_user.id).order(:updated_at, :desc).to_a
|
||||||
|
render(:trash, locals: { comments: comments, notice: flash[:notice] })
|
||||||
|
end
|
||||||
|
|
||||||
|
# find_by! against only_deleted (not the default Comment.find, which
|
||||||
|
# hides trashed rows) - restoring a comment that was never trashed, or
|
||||||
|
# someone else's, both come back as the same 404 a stranger's comment id
|
||||||
|
# would. recursive: true brings back this comment's own likes if they
|
||||||
|
# were soft-deleted alongside it by the dependent: :destroy cascade on
|
||||||
|
# Comment#destroy (Like is paranoid too - see cascade_restore_dependent_
|
||||||
|
# associations); it does not touch the comment's post either way.
|
||||||
|
def restore
|
||||||
|
comment = Comment.only_deleted.find_by!(id: params['id'].to_i)
|
||||||
|
authorize(comment, :restore?)
|
||||||
|
comment.restore(recursive: true)
|
||||||
|
flash[:notice] = t('comments.restored_notice')
|
||||||
|
redirect url_for(:trash_comments)
|
||||||
|
rescue Prism::Orm::RecordNotFound
|
||||||
|
respond_not_found(t('comments.not_found'))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Pushed to every open posts#show tab's PostActivityChannel subscription
|
||||||
|
# (see the model) - approve/reject/destroy are the only three actions
|
||||||
|
# that can change how many APPROVED comments a post has (create cannot,
|
||||||
|
# every comment starts life pending), so this is called from exactly
|
||||||
|
# those three, never from create/update.
|
||||||
|
def broadcast_comment_count(post)
|
||||||
|
# stream_for/post_id_from (see the channel) treat this as an opaque
|
||||||
|
# string key, not a real id - post.to_param (the slug) so the stream a
|
||||||
|
# client subscribes to via GET /cable?post_id=<slug> (posts/show.erb
|
||||||
|
# has no inline subscriber script yet, but blog_websocket_demo.rb
|
||||||
|
# subscribes using the same slug it got from the post's own URL)
|
||||||
|
# actually matches what gets broadcast to here.
|
||||||
|
PostActivityChannel.hub.broadcast(
|
||||||
|
PostActivityChannel.stream_for(post.to_param),
|
||||||
|
event: 'comment_count', post_id: post.to_param, count: post.comments.approved.count
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user