feat(controllers): extend posts_controller with import and create
This commit is contained in:
1 parent
15d358f447
commit
985e87fb68
1 file changed
+173
@@ -114,4 +114,177 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The reverse of #export above: bulk-creates posts from an uploaded CSV
|
||||
# (title,body columns). All the actual work - the per-row unique-slug
|
||||
# retry and the outer transaction wrapping the whole batch - lives in
|
||||
# Post.import_csv; this action is just multipart plumbing and reporting.
|
||||
def import
|
||||
file = params['file']
|
||||
unless file.is_a?(Prism::UploadedFile) && !file.original_filename.to_s.empty?
|
||||
flash[:notice] = t('posts.import_missing_file')
|
||||
redirect url_for(:posts)
|
||||
return
|
||||
end
|
||||
result = Post.import_csv(file.read, author: current_user)
|
||||
flash[:notice] = t('posts.import_notice', imported: result.imported.size, skipped: result.skipped.size)
|
||||
redirect url_for(:posts)
|
||||
end
|
||||
|
||||
# The create form's submit button says "Publish" (see posts.submit in the
|
||||
# locale files), so a post made here goes live immediately - status only
|
||||
# ever starts at the enum's draft default for a row nothing else besides
|
||||
# this action ever inserts. #unpublish/#publish (below) are how an
|
||||
# existing post is pulled back to draft and put up again.
|
||||
def create
|
||||
attrs = params.require('post').permit(:title, :body, :featured, :giveaway_slots).to_h
|
||||
attrs['status'] = :published
|
||||
attrs['cover'] = store_cover(params['post']['cover'])
|
||||
attrs['user_id'] = current_user.id
|
||||
# Post.create_with_unique_slug (see the model), not a plain Post.new+#save
|
||||
# - every post needs a slug for the unique index the add_slug_to_posts
|
||||
# migration put on posts.slug, and this is the only place besides
|
||||
# Post.import_csv that inserts a post at all.
|
||||
post = Post.create_with_unique_slug(attrs)
|
||||
|
||||
if post.persisted?
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:notice] = t('posts.created_notice', id: post.id)
|
||||
redirect url_for(:post, slug: post.to_param)
|
||||
end
|
||||
format.json { json(post.as_json(only: %i[id slug title body cover]), 201) }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
status(422)
|
||||
render(:index, locals: { posts: Post.published.order(:created_at, :desc).page(1).per(5), new_post: post, notice: nil,
|
||||
errors: post.errors.full_messages, days: 0, featured_only: false })
|
||||
end
|
||||
format.json { render_errors(post) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
post = Post.find_by!(slug: params['slug'])
|
||||
authorize(post, :update?)
|
||||
render(:edit, locals: { post: post, errors: [] })
|
||||
rescue Prism::Orm::RecordNotFound
|
||||
respond_not_found(t('posts.not_found'))
|
||||
end
|
||||
|
||||
# Diffs the submitted title/body against the persisted row WITHOUT
|
||||
# saving anything - assign() dirty-tracks the attempt, #changes captures
|
||||
# the old/new pair for each touched field, then Dirty#restore_attributes
|
||||
# reverts `post` back to exactly what #save last loaded so the rest of
|
||||
# this action (and the view it renders) can keep using the same instance
|
||||
# as a clean, still-persisted record instead of hitting the store again.
|
||||
# `attempted` is kept separately (not read back off `post`, which is now
|
||||
# clean again) so the confirm form below can still resubmit what the
|
||||
# visitor actually typed.
|
||||
def preview
|
||||
post = Post.find_by!(slug: params['slug'])
|
||||
authorize(post, :update?)
|
||||
attempted = params.require('post').permit(:title, :body, :lock_version).to_h
|
||||
post.assign(attempted)
|
||||
post.valid?
|
||||
diff = post.changes.reject { |field, _| field == 'lock_version' }
|
||||
errors = post.errors.full_messages
|
||||
post.restore_attributes
|
||||
render(:preview, locals: { post: post, diff: diff, errors: errors, attempted: attempted })
|
||||
rescue Prism::Orm::RecordNotFound
|
||||
respond_not_found(t('posts.not_found'))
|
||||
end
|
||||
|
||||
# Only assigns the attributes the edit form actually submits (not cover/
|
||||
# user_id) so Post#saved_changes below can only ever list fields the
|
||||
# visitor meant to touch, then uses that dirty-tracking snapshot to tell
|
||||
# them exactly what took effect instead of a generic "post updated".
|
||||
# `lock_version` rides along as a permitted attribute holding whatever
|
||||
# version the edit form was rendered with (not a fresh read) - see
|
||||
# Post#save's optimistic-locking check, which compares that submitted
|
||||
# value against the row's ACTUAL current version, not the just-loaded one.
|
||||
def update
|
||||
post = Post.find_by!(slug: params['slug'])
|
||||
authorize(post)
|
||||
permitted = params.require('post').permit(
|
||||
:title, :body, :featured, :giveaway_slots, :lock_version, comments_attributes: %i[id body _destroy]
|
||||
).to_h
|
||||
post.assign(permitted)
|
||||
|
||||
if post.save
|
||||
changed_fields = post.saved_changes.keys - ['updated_at', 'lock_version']
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:notice] = changed_fields.empty? ? t('posts.update_noop_notice') : t('posts.updated_notice', fields: changed_fields.join(', '))
|
||||
redirect url_for(:post, slug: post.to_param)
|
||||
end
|
||||
format.json { json(post.as_json(only: %i[id slug title body cover])) }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
status(422)
|
||||
render(:edit, locals: { post: post, errors: post.errors.full_messages })
|
||||
end
|
||||
format.json { render_errors(post) }
|
||||
end
|
||||
end
|
||||
rescue Prism::Orm::RecordNotFound
|
||||
respond_not_found(t('posts.not_found'))
|
||||
rescue Prism::Orm::StaleObjectError
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
status(409)
|
||||
render(:edit, locals: { post: Post.find_by!(slug: params['slug']), errors: [t('posts.stale_notice')] })
|
||||
end
|
||||
format.json do
|
||||
json({ errors: [jsonapi_error(status: 409, pointer: '/data/attributes/lock_version',
|
||||
title: 'is stale', detail: t('posts.stale_notice'))] }, 409)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Post#acts_as_paranoid means this call moves the post to the trash
|
||||
# instead of removing it for real - #trash/#restore below are how its
|
||||
# own author gets it back.
|
||||
def destroy
|
||||
post = Post.find_by!(slug: params['slug'])
|
||||
authorize(post)
|
||||
post.destroy
|
||||
flash[:notice] = t('posts.deleted_notice', id: post.id)
|
||||
redirect url_for(:posts)
|
||||
rescue Prism::Orm::RecordNotFound
|
||||
respond_not_found(t('posts.not_found'))
|
||||
end
|
||||
|
||||
# Only the signed-in author's own trashed posts - Post.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
|
||||
posts = Post.only_deleted.where(user_id: current_user.id).order(:updated_at, :desc).to_a
|
||||
render(:trash, locals: { posts: posts, notice: flash[:notice] })
|
||||
end
|
||||
|
||||
# find_by! against only_deleted (not the default Post.find, which hides
|
||||
# trashed rows) - restoring a post that was never trashed, or someone
|
||||
# else's, both come back as the same 404 a stranger's post id would.
|
||||
def restore
|
||||
# slug survives a soft delete unchanged (it's just a column, untouched
|
||||
# by deleted_at) - no numeric coercion needed here the way the old
|
||||
# id-keyed lookup required, since a String slug compares correctly
|
||||
# against row['slug'] (also a String) with plain ==.
|
||||
post = Post.only_deleted.find_by!(slug: params['slug'])
|
||||
authorize(post, :restore?)
|
||||
# recursive: true - a plain post.restore would only bring the post's
|
||||
# own row back and leave its comments/likes trashed forever, since
|
||||
# they were soft-deleted as a side effect of dependent: :destroy when
|
||||
# the post itself was trashed (see Post#acts_as_paranoid).
|
||||
post.restore(recursive: true)
|
||||
flash[:notice] = t('posts.restored_notice', id: post.id)
|
||||
redirect url_for(:trash_posts)
|
||||
rescue Prism::Orm::RecordNotFound
|
||||
end
|
||||
Reference in new issue
Block a user