feat(controllers): extend posts_controller with PostsController
This commit is contained in:
1 parent
c66a866312
commit
15d358f447
1 file changed
+106
-13
@@ -1,24 +1,117 @@
|
||||
class PostsController < Prism::Controller
|
||||
layout :application
|
||||
require 'csv'
|
||||
require 'fileutils'
|
||||
|
||||
class PostsController < ApplicationController
|
||||
before_action :require_authentication,
|
||||
only: %i[create edit update destroy preview publish unpublish trash restore import move_up move_down]
|
||||
|
||||
# Same content-aware etag idea as #show below, but over the whole page of
|
||||
# posts: current_page/total_count catch pagination shifting (a post
|
||||
# created or destroyed elsewhere moves what belongs on this page even
|
||||
# when none of the posts actually ON this page changed), and the id/
|
||||
# updated_at pairs catch edits to any of them.
|
||||
#
|
||||
# `.published` is a scope generated by Post's `enum :status` - every post
|
||||
# starts as a draft (see the model), so a freshly created one is invisible
|
||||
# here until its author publishes it from posts#show.
|
||||
def index
|
||||
render(:index, locals: { posts: Post.order(:created_at, :desc), notice: flash[:notice], errors: [] })
|
||||
# `days` is an optional "created in the last N days" filter (see
|
||||
# posts.filter_* links in the view) chained onto .published BEFORE
|
||||
# .order/.page - Post.created_within hits the range-capable secondary
|
||||
# index on created_at (Store#lookup_by_index_range) the same way
|
||||
# .published already hits the one on status, so neither filter alone
|
||||
# nor the two combined ever falls back to a full table scan.
|
||||
days = params['days'].to_i
|
||||
# `params['featured']` is the raw query-string String Rack always hands
|
||||
# a controller ("1" or nil) - cast through the same TypeCasting a form
|
||||
# POST would use, rather than a bespoke `== '1'` check here, so this
|
||||
# filter and the checkbox in _form.erb agree on what counts as true.
|
||||
featured_only = params['featured'] && Prism::Orm::TypeCasting.cast(:boolean, params['featured'])
|
||||
scope = Post.published
|
||||
scope = scope.created_within(days) if days.positive?
|
||||
scope = scope.featured if featured_only
|
||||
# The featured view is manually curated (Post#acts_as_list, scoped by
|
||||
# featured - see the model and #move_up/#move_down above), so it sorts
|
||||
# by that editorial position instead of recency like every other view
|
||||
# of this same index action.
|
||||
posts = featured_only ? scope.order(:position, :asc).page(params['page']).per(5)
|
||||
: scope.order(:created_at, :desc).page(params['page']).per(5)
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
notice = flash[:notice]
|
||||
# current_user&.id rides along in the etag, same audit fix as #show
|
||||
# below: this page's "new post" form and the layout's signed-in-as
|
||||
# banner are both gated on user_signed_in?/current_user, so without
|
||||
# this a shared cache keying purely on URL+ETag could serve one
|
||||
# visitor's signed-in body back as a 304 to a different, or
|
||||
# signed-out, visitor whose If-None-Match happened to match. `days`
|
||||
# rides along too so switching the filter without the post set
|
||||
# actually changing (e.g. nothing published in the last 7 days
|
||||
# either way) still forces a fresh render, not a stale 304 for the
|
||||
# wrong filter. `featured_only` rides along for the same reason -
|
||||
# and so does each post's `position` (see Post#acts_as_list): a
|
||||
# #move_up/#move_down reorder touches ONLY the position column,
|
||||
# never updated_at, so without it in the etag two requests with the
|
||||
# exact same set of ids/updated_at pairs but a different editorial
|
||||
# order would wrongly hash to the same ETag and serve a stale 304.
|
||||
fresh_when(etag: [posts.current_page, posts.total_count,
|
||||
posts.to_a.map { |p| [p.id, p.updated_at, p.position] }, notice,
|
||||
current_user&.id, days, featured_only])
|
||||
render(:index, locals: { posts: posts, new_post: Post.new, notice: notice, errors: [], days: days,
|
||||
featured_only: featured_only })
|
||||
end
|
||||
format.json do
|
||||
json(posts.as_json(only: %i[id slug title body cover]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# `notice` rides along in the etag components (not just post/comments
|
||||
# state) so a pending flash message - which only exists for the one
|
||||
# request right after a redirect - always forces a fresh render instead
|
||||
# of being hidden behind a 304 that matches an older, notice-less copy
|
||||
# of this same page a client happened to have cached.
|
||||
def show
|
||||
render(:show, locals: { post: Post.find(params['id']) })
|
||||
post = Post.find_by!(slug: params['slug'])
|
||||
comments = post.comments.includes(:likes).visible_to(current_user, post.user_id).order(:created_at, :desc).to_a
|
||||
notice = flash[:notice]
|
||||
# current_user.id rides along in the etag, not just the post/comments
|
||||
# data: this page also shows moderator-only controls (edit/publish/
|
||||
# delete, comment approve/reject) and viewer-only pending comments, so
|
||||
# two different visitors who happen to see the identical VISIBLE
|
||||
# comment set must still never be served each other's cached 304 body.
|
||||
# post.claims_count rides along too - a giveaway claim (see
|
||||
# ClaimsController#create) changes #giveaway_remaining/
|
||||
# #giveaway_sold_out? and therefore this page's body without ever
|
||||
# touching post.updated_at (Claim has no touch: true, on purpose: a
|
||||
# claim is not an edit to the post itself), so without it here a
|
||||
# cached 304 would keep serving a stale remaining-slots count and a
|
||||
# stale claim button after someone had already claimed the last slot.
|
||||
fresh_when(etag: [post.id, post.updated_at, post.claims_count, current_user&.id,
|
||||
comments.map { |c| [c.id, c.updated_at] }, notice])
|
||||
render(:show, locals: { post: post, comments: comments, new_comment: Comment.new, notice: notice })
|
||||
rescue Prism::Orm::RecordNotFound
|
||||
halt(404, 'Post not found')
|
||||
respond_not_found(t('posts.not_found'))
|
||||
end
|
||||
|
||||
def create
|
||||
post = Post.new(title: params['title'], body: params['body'])
|
||||
if post.save
|
||||
flash[:notice] = "Post ##{post.id} created"
|
||||
redirect url_for(:posts)
|
||||
else
|
||||
status(422)
|
||||
render(:index, locals: { posts: Post.order(:created_at, :desc), notice: nil, errors: post.errors.full_messages })
|
||||
# Streamed (Prism::Controller#stream, see lib/prism/streaming_body.rb),
|
||||
# not rendered into one String first - a real Rails app faces the exact
|
||||
# same shape of problem exporting a large table, and #index already
|
||||
# proves .published/.created_within hit a secondary index instead of a
|
||||
# full scan, so this is the same query with the row-by-row cost paid to
|
||||
# the client incrementally instead of all at once in memory. The header
|
||||
# row is written (and would reach the client) before the last matching
|
||||
# post has even been read out of the store.
|
||||
def export
|
||||
days = params['days'].to_i
|
||||
scope = Post.published
|
||||
scope = scope.created_within(days) if days.positive?
|
||||
headers('Content-Type' => 'text/csv')
|
||||
stream do |out|
|
||||
out.write(CSV.generate_line(%w[id title created_at]))
|
||||
scope.order(:created_at, :desc).find_each do |post|
|
||||
out.write(CSV.generate_line([post.id, post.title, post.created_at.utc.iso8601]))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user