このコミットが含まれているのは:
@@ -43,25 +43,12 @@ class ApplicationController < ActionController::API
|
||||
render json: { errors: [error] }, status:
|
||||
end
|
||||
|
||||
def render_model_errors record, status: :unprocessable_entity
|
||||
errors =
|
||||
record.errors.map do |error|
|
||||
{ code: error.type.to_s,
|
||||
field: error.attribute.to_s,
|
||||
message: error.full_message }
|
||||
end
|
||||
|
||||
errors = [{ code: 'invalid', message: '入力を確認してください.' }] if errors.empty?
|
||||
|
||||
render json: { errors: }, status:
|
||||
end
|
||||
|
||||
def render_record_invalid error
|
||||
render_model_errors(error.record)
|
||||
render_validation_error error.record
|
||||
end
|
||||
|
||||
def render_record_not_unique _error = nil
|
||||
render_unprocessable_entity('既に存在してゐます.', code: :taken)
|
||||
render_validation_error base: ['すでに存在してゐます.']
|
||||
end
|
||||
|
||||
def reject_banned_ip_address!
|
||||
@@ -77,27 +64,27 @@ class ApplicationController < ActionController::API
|
||||
head :forbidden
|
||||
end
|
||||
|
||||
def render_validation_error record = nil, fields: { }, base: []
|
||||
def render_validation_error record = nil, fields: { }, base: [], status: :unprocessable_entity
|
||||
errors = { }
|
||||
|
||||
if record
|
||||
record.errors.messages.each do |attr, messages|
|
||||
errors[attr] ||= []
|
||||
errors[attr].concat(messages)
|
||||
record.errors.each do |error|
|
||||
errors[error.attribute] ||= []
|
||||
errors[error.attribute] << error.message
|
||||
end
|
||||
end
|
||||
|
||||
fields.each do |attr, messages|
|
||||
errors[attr] ||= []
|
||||
errors[attr].concat(Array(messages))
|
||||
errors[attr.to_sym] ||= []
|
||||
errors[attr.to_sym].concat(Array(messages))
|
||||
end
|
||||
|
||||
base_errors = Array(base) - Array(errors.delete(:base))
|
||||
base_errors = Array(base) + Array(errors.delete(:base))
|
||||
|
||||
render json: { type: 'validation_error',
|
||||
message: '入力内容を確認してください.',
|
||||
errors:,
|
||||
base_errors: },
|
||||
status: :unprocessable_entity
|
||||
status:
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,7 +44,7 @@ class PostsController < ApplicationController
|
||||
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: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
.preload(:uploaded_user, tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
.with_attached_thumbnail
|
||||
|
||||
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
||||
@@ -95,7 +95,9 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def random
|
||||
post = filtered_posts.preload(tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
post = filtered_posts.preload(:uploaded_user,
|
||||
tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
.with_attached_thumbnail
|
||||
.order('RAND()')
|
||||
.first
|
||||
return head :not_found unless post
|
||||
@@ -104,12 +106,24 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
post = Post.includes(tags: [:deerjikists, :materials, { tag_name: :wiki_page }]).find_by(id: params[:id])
|
||||
post =
|
||||
Post
|
||||
.includes(:uploaded_user, tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
.with_attached_thumbnail
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless post
|
||||
|
||||
render json: PostRepr.base(post, current_user)
|
||||
.merge(tags: build_tag_tree_for(post.tags),
|
||||
related: PostRepr.many(post.related(limit: 20)))
|
||||
parent_posts = post.parents.with_attached_thumbnail.order(:id).to_a
|
||||
child_posts = post.children.with_attached_thumbnail.order(:id).to_a
|
||||
sibling_posts = sibling_posts_by_parent(parent_posts.map(&:id))
|
||||
related = post.related(limit: 20).to_a
|
||||
|
||||
render json: PostRepr.detail(post, current_user,
|
||||
parent_posts:,
|
||||
child_posts:,
|
||||
sibling_posts:,
|
||||
related:)
|
||||
.merge(tags: build_tag_tree_for(post.tags))
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -148,11 +162,11 @@ class PostsController < ApplicationController
|
||||
post.reload
|
||||
render json: PostRepr.base(post), status: :created
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
render_bad_request('ニコニコ・タグは直接指定できません.', field: :tags)
|
||||
render_validation_error fields: { tags: 'ニコニコ・タグは直接指定できません.' }
|
||||
rescue ArgumentError => e
|
||||
render_unprocessable_entity(e.message)
|
||||
render_validation_error fields: { parent_post_ids: [e.message] }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render_model_errors(e.record)
|
||||
render_post_form_record_invalid e.record
|
||||
end
|
||||
|
||||
def viewed
|
||||
@@ -238,11 +252,11 @@ class PostsController < ApplicationController
|
||||
json['tags'] = build_tag_tree_for(post.tags)
|
||||
render json:, status: :ok
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
render_bad_request('ニコニコ・タグは直接指定できません.', field: :tags)
|
||||
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
|
||||
rescue ArgumentError => e
|
||||
render_validation_error(fields: { parent_post_ids: [e.message] })
|
||||
render_validation_error fields: { parent_post_ids: [e.message] }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render_validation_error(e.record)
|
||||
render_post_form_record_invalid e.record
|
||||
end
|
||||
|
||||
def changes
|
||||
@@ -385,7 +399,7 @@ class PostsController < ApplicationController
|
||||
return nil unless tag
|
||||
|
||||
if path.include?(tag_id)
|
||||
return TagRepr.base(tag).merge(children: [])
|
||||
return TagRepr.inline(tag).merge(children: [])
|
||||
end
|
||||
|
||||
if memo.key?(tag_id)
|
||||
@@ -397,12 +411,26 @@ class PostsController < ApplicationController
|
||||
|
||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
||||
|
||||
memo[tag_id] = TagRepr.base(tag).merge(children:)
|
||||
memo[tag_id] = TagRepr.inline(tag).merge(children:)
|
||||
end
|
||||
|
||||
root_ids.filter_map { |id| build_node.call(id, []) }
|
||||
end
|
||||
|
||||
def sibling_posts_by_parent parent_post_ids
|
||||
return { } if parent_post_ids.blank?
|
||||
|
||||
implications =
|
||||
PostImplication
|
||||
.where(parent_post_id: parent_post_ids)
|
||||
.includes(post: { thumbnail_attachment: :blob })
|
||||
.order(:parent_post_id, :post_id)
|
||||
|
||||
implications.group_by(&:parent_post_id).transform_values { |items|
|
||||
items.map(&:post)
|
||||
}
|
||||
end
|
||||
|
||||
def parse_parent_post_ids
|
||||
raise ArgumentError, 'parent_post_ids は必須です.' unless params.key?(:parent_post_ids)
|
||||
|
||||
@@ -416,7 +444,7 @@ class PostsController < ApplicationController
|
||||
|
||||
def sync_parent_posts! post, parent_post_ids
|
||||
if parent_post_ids.include?(post.id)
|
||||
post.errors.add(:parent_post_ids, '自分自身を親投稿にはできません.')
|
||||
post.errors.add :parent_post_ids, '自分自身を親投稿にはできません.'
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
|
||||
@@ -424,7 +452,8 @@ class PostsController < ApplicationController
|
||||
missing_ids = parent_post_ids - existing_ids
|
||||
|
||||
if missing_ids.present?
|
||||
post.errors.add(:base, "存在しない親投稿 ID があります: #{ missing_ids.join(' ') }")
|
||||
post.errors.add :parent_post_ids,
|
||||
"存在しない親投稿 ID があります: #{ missing_ids.join(' ') }"
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
|
||||
@@ -640,4 +669,14 @@ class PostsController < ApplicationController
|
||||
|
||||
merged.uniq.sort
|
||||
end
|
||||
|
||||
def render_post_form_record_invalid record
|
||||
if e.record.is_a?(TagName) || e.record.is_a?(Tag)
|
||||
render_validation_error(fields: { tags: e.record.errors.full_messages.map { |message|
|
||||
"タグ名 “#{ e.record.name }”: #{ message }"
|
||||
} })
|
||||
else
|
||||
render_validation_error(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -94,7 +94,7 @@ class Post < ApplicationRecord
|
||||
return if !(f) || !(b)
|
||||
|
||||
if f >= b
|
||||
errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.'
|
||||
errors.add :original_created_at, 'オリジナルの作成日時の順番がをかしぃです.'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,20 +2,65 @@
|
||||
|
||||
|
||||
module PostRepr
|
||||
BASE = { include: { tags: TagRepr::BASE, uploaded_user: UserRepr::BASE },
|
||||
methods: [:parent_posts, :child_posts, :sibling_posts] }.freeze
|
||||
BASE_FIELDS = [
|
||||
:id,
|
||||
:version_no,
|
||||
:url,
|
||||
:title,
|
||||
:thumbnail_base,
|
||||
:original_created_from,
|
||||
:original_created_before,
|
||||
:created_at,
|
||||
:updated_at
|
||||
].freeze
|
||||
|
||||
module_function
|
||||
|
||||
def base post, current_user = nil
|
||||
json = post.as_json(BASE)
|
||||
return json.merge(viewed: false) unless current_user
|
||||
json = common(post)
|
||||
json['tags'] = tag_json(post.tags)
|
||||
json['uploaded_user'] = post.uploaded_user && UserRepr.base(post.uploaded_user)
|
||||
json['viewed'] = current_user ? current_user.viewed?(post) : false
|
||||
json
|
||||
end
|
||||
|
||||
viewed = current_user.viewed?(post)
|
||||
json.merge(viewed:)
|
||||
def detail post, current_user = nil, parent_posts: [], child_posts: [],
|
||||
sibling_posts: { }, related: []
|
||||
base(post, current_user).merge(
|
||||
'parent_posts' => cards(parent_posts),
|
||||
'child_posts' => cards(child_posts),
|
||||
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
|
||||
cards(posts)
|
||||
},
|
||||
'related' => cards(related))
|
||||
end
|
||||
|
||||
def card post
|
||||
common(post).merge('parent_posts' => [], 'child_posts' => [])
|
||||
end
|
||||
|
||||
def cards posts
|
||||
posts.map { |post| card(post) }
|
||||
end
|
||||
|
||||
def many posts, current_user = nil
|
||||
posts.map { |p| base(p, current_user) }
|
||||
end
|
||||
|
||||
def common post
|
||||
BASE_FIELDS.to_h { |field| [field.to_s, post.public_send(field)] }
|
||||
.merge('thumbnail' => thumbnail_url(post))
|
||||
end
|
||||
|
||||
def tag_json tags
|
||||
tags.map { |tag| TagRepr.inline(tag) }
|
||||
end
|
||||
|
||||
def thumbnail_url post
|
||||
return nil unless post.thumbnail.attached?
|
||||
|
||||
Rails.application.routes.url_helpers.rails_blob_url(post.thumbnail, only_path: false)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,5 +12,9 @@ module TagRepr
|
||||
parents: tag.parents.map { _1.as_json(BASE) })
|
||||
end
|
||||
|
||||
def inline tag
|
||||
tag.as_json(BASE).merge(aliases: [], parents: [])
|
||||
end
|
||||
|
||||
def many(tags) = tags.map { |t| base(t) }
|
||||
end
|
||||
|
||||
@@ -57,6 +57,23 @@ RSpec.describe 'Posts API', type: :request do
|
||||
post_write_params({ base_version_no: base_version.version_no }.merge(params))
|
||||
end
|
||||
|
||||
def count_sql_queries
|
||||
count = 0
|
||||
|
||||
callback = lambda do |_name, _started, _finished, _id, payload|
|
||||
next if payload[:cached]
|
||||
next if ['SCHEMA', 'TRANSACTION'].include?(payload[:name])
|
||||
|
||||
count += 1
|
||||
end
|
||||
|
||||
ActiveSupport::Notifications.subscribed(callback, 'sql.active_record') do
|
||||
yield
|
||||
end
|
||||
|
||||
count
|
||||
end
|
||||
|
||||
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
|
||||
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
|
||||
|
||||
@@ -558,6 +575,59 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(sibling_ids).to include(sibling_post.id)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not issue a query per tag or related post' do
|
||||
user = create_member_user!
|
||||
|
||||
tags =
|
||||
15.times.map do |i|
|
||||
tag_name = TagName.create!(name: "show_query_tag_#{ i }")
|
||||
tag = Tag.create!(tag_name:, category: :general)
|
||||
TagName.create!(name: "show_query_alias_#{ i }", canonical: tag_name)
|
||||
PostTag.create!(post: post_record, tag:)
|
||||
tag
|
||||
end
|
||||
|
||||
tags.each_cons(2) do |parent_tag, child_tag|
|
||||
TagImplication.create!(parent_tag:, tag: child_tag)
|
||||
end
|
||||
|
||||
parent_post = Post.create!(
|
||||
title: 'query parent post',
|
||||
url: 'https://example.com/query-parent-post'
|
||||
)
|
||||
sibling_post = Post.create!(
|
||||
title: 'query sibling post',
|
||||
url: 'https://example.com/query-sibling-post'
|
||||
)
|
||||
child_post = Post.create!(
|
||||
title: 'query child post',
|
||||
url: 'https://example.com/query-child-post'
|
||||
)
|
||||
|
||||
PostImplication.create!(post: post_record, parent_post:)
|
||||
PostImplication.create!(post: sibling_post, parent_post:)
|
||||
PostImplication.create!(post: child_post, parent_post: post_record)
|
||||
|
||||
20.times do |i|
|
||||
related_post = Post.create!(
|
||||
title: "query related post #{ i }",
|
||||
url: "https://example.com/query-related-post-#{ i }"
|
||||
)
|
||||
PostSimilarity.create!(post: post_record,
|
||||
target_post: related_post,
|
||||
cos: 1.0 - (i / 100.0))
|
||||
end
|
||||
|
||||
query_count =
|
||||
count_sql_queries do
|
||||
get "/posts/#{ post_record.id }",
|
||||
headers: { 'X-Transfer-Code' => user.inheritance_code }
|
||||
end
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(query_count).to be <= 45
|
||||
end
|
||||
end
|
||||
|
||||
context 'when post does not exist' do
|
||||
|
||||
新しい課題から参照
ユーザをブロックする