|
- class PostsController < ApplicationController
- Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)
-
- def index
- url = params[:url].presence
- title = params[:title].presence
- original_created_from = params[:original_created_from].presence
- original_created_to = params[:original_created_to].presence
- created_between = params[:created_from].presence, params[:created_to].presence
- updated_between = params[:updated_from].presence, params[:updated_to].presence
-
- order = params[:order].to_s.split(':', 2).map(&:strip)
- unless order[0].in?(['title', 'url', 'original_created_at', 'created_at', 'updated_at'])
- order[0] = 'original_created_at'
- end
- unless order[1].in?(['asc', 'desc'])
- order[1] =
- if order[0].in?(['title', 'url'])
- 'asc'
- else
- 'desc'
- end
- end
-
- page = (params[:page].presence || 1).to_i
- limit = (params[:limit].presence || 20).to_i
-
- page = 1 if page < 1
- limit = 1 if limit < 1
-
- offset = (page - 1) * limit
-
- pt_max_sql =
- PostTag
- .select('post_id, MAX(updated_at) AS max_updated_at')
- .group('post_id')
- .to_sql
-
- updated_at_all_sql =
- 'GREATEST(posts.updated_at,' +
- 'COALESCE(pt_max.max_updated_at, posts.updated_at))'
-
- q =
- filtered_posts
- .joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
- .reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
- .preload(tags: { tag_name: :wiki_page })
- .with_attached_thumbnail
-
- q = q.where('posts.url LIKE ?', "%#{ url }%") if url
- q = q.where('posts.title LIKE ?', "%#{ title }%") if title
- if original_created_from
- q = q.where('posts.original_created_before > ?', original_created_from)
- end
- if original_created_to
- q = q.where('posts.original_created_from <= ?', original_created_to)
- end
- q = q.where('posts.created_at >= ?', created_between[0]) if created_between[0]
- q = q.where('posts.created_at <= ?', created_between[1]) if created_between[1]
- if updated_between[0]
- q = q.where("#{ updated_at_all_sql } >= ?", updated_between[0])
- end
- if updated_between[1]
- q = q.where("#{ updated_at_all_sql } <= ?", updated_between[1])
- end
-
- sort_sql =
- if order[0] == 'original_created_at'
- 'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' +
- 'posts.original_created_from,' +
- 'posts.created_at) ' +
- order[1]
- else
- "posts.#{ order[0] } #{ order[1] }"
- end
- posts = q.order(Arel.sql("#{ sort_sql }")).limit(limit).offset(offset).to_a
-
- q = q.except(:select, :order)
-
- render json: { posts: posts.map { |post|
- PostRepr.base(post).merge(updated_at: post.updated_at_all).tap do |json|
- json['thumbnail'] =
- if post.thumbnail.attached?
- rails_storage_proxy_url(post.thumbnail, only_path: false)
- else
- nil
- end
- end
- }, count: q.group_values.present? ? q.count.size : q.count }
- end
-
- def random
- post = filtered_posts.preload(tags: { tag_name: :wiki_page })
- .order('RAND()')
- .first
- return head :not_found unless post
-
- viewed = current_user&.viewed?(post) || false
-
- render json: PostRepr.base(post).merge(viewed:)
- end
-
- def show
- post = Post.includes(tags: { tag_name: :wiki_page }).find_by(id: params[:id])
- return head :not_found unless post
-
- viewed = current_user&.viewed?(post) || false
-
- json = post.as_json
- json['tags'] = build_tag_tree_for(post.tags)
- json['related'] = post.related(limit: 20)
- json['viewed'] = viewed
-
- render json:
- end
-
- def create
- return head :unauthorized unless current_user
- return head :forbidden unless current_user.member?
-
- # TODO: サイトに応じて thumbnail_base 設定
- title = params[:title].presence
- url = params[:url]
- thumbnail = params[:thumbnail]
- tag_names = params[:tags].to_s.split
- original_created_from = params[:original_created_from]
- original_created_before = params[:original_created_before]
-
- post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user,
- original_created_from:, original_created_before:)
- post.thumbnail.attach(thumbnail)
- if post.save
- post.resized_thumbnail!
- tags = Tag.normalise_tags(tag_names)
- tags = Tag.expand_parent_tags(tags)
- sync_post_tags!(post, tags)
-
- post.reload
- render json: PostRepr.base(post), status: :created
- else
- render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
- end
- rescue Tag::NicoTagNormalisationError
- head :bad_request
- end
-
- def viewed
- return head :unauthorized unless current_user
-
- current_user.viewed_posts << Post.find(params[:id])
- head :no_content
- end
-
- def unviewed
- return head :unauthorized unless current_user
-
- current_user.viewed_posts.delete(Post.find(params[:id]))
- head :no_content
- end
-
- def update
- return head :unauthorized unless current_user
- return head :forbidden unless current_user.member?
-
- title = params[:title].presence
- tag_names = params[:tags].to_s.split
- original_created_from = params[:original_created_from]
- original_created_before = params[:original_created_before]
-
- post = Post.find(params[:id].to_i)
- if post.update(title:, original_created_from:, original_created_before:)
- tags = post.tags.where(category: 'nico').to_a +
- Tag.normalise_tags(tag_names, with_tagme: false)
- tags = Tag.expand_parent_tags(tags)
- sync_post_tags!(post, tags)
-
- post.reload
- json = post.as_json
- json['tags'] = build_tag_tree_for(post.tags)
- render json:, status: :ok
- else
- render json: post.errors, status: :unprocessable_entity
- end
- rescue Tag::NicoTagNormalisationError
- head :bad_request
- end
-
- def changes
- id = params[:id].presence
- page = (params[:page].presence || 1).to_i
- limit = (params[:limit].presence || 20).to_i
-
- page = 1 if page < 1
- limit = 1 if limit < 1
-
- offset = (page - 1) * limit
-
- pts = PostTag.with_discarded
- pts = pts.where(post_id: id) if id.present?
- pts = pts.includes(:post, :created_user, :deleted_user,
- tag: { tag_name: :wiki_page })
-
- events = []
- pts.each do |pt|
- tag = TagRepr.base(pt.tag)
- post = pt.post
-
- events << Event.new(
- post:,
- tag:,
- user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
- change_type: 'add',
- timestamp: pt.created_at)
-
- if pt.discarded_at
- events << Event.new(
- post:,
- tag:,
- user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
- change_type: 'remove',
- timestamp: pt.discarded_at)
- end
- end
- events.sort_by!(&:timestamp)
- events.reverse!
-
- render json: { changes: (events.slice(offset, limit) || []).as_json, count: events.size }
- end
-
- private
-
- def filtered_posts
- tag_names = params[:tags].to_s.split
- match_type = params[:match]
- if tag_names.present?
- filter_posts_by_tags(tag_names, match_type)
- else
- Post.all
- end
- end
-
- def filter_posts_by_tags tag_names, match_type
- tag_names = TagName.canonicalise(tag_names)
-
- posts = Post.joins(tags: :tag_name)
-
- if match_type == 'any'
- posts.where(tag_names: { name: tag_names }).distinct
- else
- posts.where(tag_names: { name: tag_names })
- .group('posts.id')
- .having('COUNT(DISTINCT tag_names.id) = ?', tag_names.uniq.size)
- end
- end
-
- def sync_post_tags! post, desired_tags
- desired_tags.each do |t|
- t.save! if t.new_record?
- end
-
- desired_ids = desired_tags.map(&:id).to_set
- current_ids = post.tags.pluck(:id).to_set
-
- to_add = desired_ids - current_ids
- to_remove = current_ids - desired_ids
-
- Tag.where(id: to_add).find_each do |tag|
- begin
- PostTag.create!(post:, tag:, created_user: current_user)
- rescue ActiveRecord::RecordNotUnique
- ;
- end
- end
-
- PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
- pt.discard_by!(current_user)
- end
- end
-
- def build_tag_tree_for tags
- tags = tags.to_a
- tag_ids = tags.map(&:id)
-
- implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids)
-
- children_ids_by_parent = Hash.new { |h, k| h[k] = [] }
- implications.each do |imp|
- children_ids_by_parent[imp.parent_tag_id] << imp.tag_id
- end
-
- child_ids = children_ids_by_parent.values.flatten.uniq
-
- root_ids = tag_ids - child_ids
-
- tags_by_id = tags.index_by(&:id)
-
- memo = { }
-
- build_node = -> tag_id, path do
- tag = tags_by_id[tag_id]
- return nil unless tag
-
- if path.include?(tag_id)
- return TagRepr.base(tag).merge(children: [])
- end
-
- if memo.key?(tag_id)
- return memo[tag_id]
- end
-
- new_path = path + [tag_id]
- child_ids = children_ids_by_parent[tag_id] || []
-
- children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
-
- memo[tag_id] = TagRepr.base(tag).merge(children:)
- end
-
- root_ids.filter_map { |id| build_node.call(id, []) }
- end
- end
|