コミットを比較

..
4 コミット
作成者 SHA1 メッセージ 日付
Codex Night Worker a02037599a AI: address review on PR #462 2026-11-12 19:49:55 +09:00
Codex Night Worker 09df6d740a AI: address review on PR #462 2026-11-12 19:47:49 +09:00
Codex Night Worker df9e98132e AI: address review on PR #462 2026-11-12 19:45:42 +09:00
Codex Night Worker 930817d077 AI: address issue #450 2026-11-12 19:13:32 +09:00
17個のファイルの変更、13行の追加、225行の削除
-1
ファイルの表示
@@ -5,7 +5,6 @@ class MaterialVersionsController < ApplicationController
page = 1 if page < 1 page = 1 if page < 1
limit = 1 if limit < 1 limit = 1 if limit < 1
limit = 100 if limit > 100
offset = (page - 1) * limit offset = (page - 1) * limit
-1
ファイルの表示
@@ -14,7 +14,6 @@ class MaterialsController < ApplicationController
page = 1 if page < 1 page = 1 if page < 1
limit = 1 if limit < 1 limit = 1 if limit < 1
limit = 100 if limit > 100
offset = (page - 1) * limit offset = (page - 1) * limit
-1
ファイルの表示
@@ -15,7 +15,6 @@ class NicoTagsController < ApplicationController
page = 1 if page < 1 page = 1 if page < 1
limit = 1 if limit < 1 limit = 1 if limit < 1
limit = 100 if limit > 100
post_tag_max_sql = post_tag_max_sql =
PostExternalTag PostExternalTag
-1
ファイルの表示
@@ -11,7 +11,6 @@ class PostVersionsController < ApplicationController
page = 1 if page < 1 page = 1 if page < 1
limit = 1 if limit < 1 limit = 1 if limit < 1
limit = 100 if limit > 100
offset = (page - 1) * limit offset = (page - 1) * limit
+6 -7
ファイルの表示
@@ -34,11 +34,10 @@ class PostsController < ApplicationController
page = 1 if page < 1 page = 1 if page < 1
limit = 1 if limit < 1 limit = 1 if limit < 1
limit = 100 if limit > 100
offset = (page - 1) * limit offset = (page - 1) * limit
needs_history_join = order[0] == 'updated_at' || updated_between.any?(&:present?) history_join_required = order[0] == 'updated_at' || updated_between.any?(&:present?)
q = q =
filtered_posts(locale) filtered_posts(locale)
@@ -58,8 +57,8 @@ class PostsController < ApplicationController
q = q.where('posts.created_at >= ?', created_between[0]) if created_between[0] 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] q = q.where('posts.created_at <= ?', created_between[1]) if created_between[1]
count_scope = q count_scope = q
if needs_history_join if history_join_required
history_times = HistoryAccess.visible(PostVersion.all) history_times = PostVersion.all
.select('post_id, MAX(created_at) AS latest_at').group(:post_id) .select('post_id, MAX(created_at) AS latest_at').group(:post_id)
q = q.joins("LEFT JOIN (#{ history_times.to_sql }) history_times " + q = q.joins("LEFT JOIN (#{ history_times.to_sql }) history_times " +
'ON history_times.post_id = posts.id') 'ON history_times.post_id = posts.id')
@@ -73,7 +72,7 @@ class PostsController < ApplicationController
q = q.where("#{ updated_at_all_sql } <= ?", updated_between[1]) q = q.where("#{ updated_at_all_sql } <= ?", updated_between[1])
end end
count_scope = q if updated_between.any?(&:present?) count_scope = q if updated_between.any?(&:present?)
if needs_history_join if history_join_required
q = q.select('posts.*', "#{ updated_at_all_sql } AS updated_at") q = q.select('posts.*', "#{ updated_at_all_sql } AS updated_at")
end end
@@ -93,10 +92,10 @@ class PostsController < ApplicationController
.offset(offset) .offset(offset)
.to_a .to_a
latest_version_times = latest_version_times =
if needs_history_join if history_join_required
{ } { }
else else
HistoryAccess.visible(PostVersion.where(post_id: posts.map(&:id))) PostVersion.where(post_id: posts.map(&:id))
.group(:post_id).maximum(:created_at) .group(:post_id).maximum(:created_at)
end end
-1
ファイルの表示
@@ -49,7 +49,6 @@ class TagDescriptionsController < ApplicationController
private private
def representation description def representation description
HistoryAccess.require_current!(description, TagDescriptionVersion)
description.as_json(only: [ description.as_json(only: [
:tag_id, :tag_id,
:language_code, :language_code,
+1 -1
ファイルの表示
@@ -1,7 +1,7 @@
class TagGroupsController < ApplicationController class TagGroupsController < ApplicationController
def index def index
locale = resolve_locale! locale = resolve_locale!
groups = HistoryAccess.current(TagGroup.all, TagGroupVersion).includes(:created_by_user, :updated_by_user).order(:id) groups = TagGroup.includes(:created_by_user, :updated_by_user).order(:id)
render json: TagGroupRepr.many(locale, groups.to_a) render json: TagGroupRepr.many(locale, groups.to_a)
end end
-1
ファイルの表示
@@ -7,7 +7,6 @@ class TagVersionsController < ApplicationController
page = 1 if page < 1 page = 1 if page < 1
limit = 1 if limit < 1 limit = 1 if limit < 1
limit = 100 if limit > 100
offset = (page - 1) * limit offset = (page - 1) * limit
-1
ファイルの表示
@@ -33,7 +33,6 @@ class TagsController < ApplicationController
page = 1 if page < 1 page = 1 if page < 1
limit = 1 if limit < 1 limit = 1 if limit < 1
limit = 100 if limit > 100
offset = (page - 1) * limit offset = (page - 1) * limit
-2
ファイルの表示
@@ -2,7 +2,6 @@ class TheatreCommentsController < ApplicationController
def index def index
limit = params[:limit].to_i limit = params[:limit].to_i
limit = 20 if limit <= 0 limit = 20 if limit <= 0
limit = 100 if limit > 100
no_gt = params[:no_gt].to_i no_gt = params[:no_gt].to_i
no_gt = 0 if no_gt < 0 no_gt = 0 if no_gt < 0
@@ -10,7 +9,6 @@ class TheatreCommentsController < ApplicationController
comments = TheatreComment comments = TheatreComment
.where(theatre_id: params[:theatre_id]) .where(theatre_id: params[:theatre_id])
.where('no > ?', no_gt) .where('no > ?', no_gt)
.includes(:user)
.order(no: :desc) .order(no: :desc)
.limit(limit) .limit(limit)
+1 -2
ファイルの表示
@@ -2,7 +2,6 @@ class TheatreProgrammesController < ApplicationController
def index def index
limit = params[:limit].to_i limit = params[:limit].to_i
limit = 100 if limit <= 0 limit = 100 if limit <= 0
limit = 100 if limit > 100
position_gt = params[:position_gt].to_i position_gt = params[:position_gt].to_i
position_gt = 0 if position_gt < 0 position_gt = 0 if position_gt < 0
@@ -11,7 +10,7 @@ class TheatreProgrammesController < ApplicationController
.where(theatre_id: params[:theatre_id]) .where(theatre_id: params[:theatre_id])
.where('position > ?', position_gt) .where('position > ?', position_gt)
.includes(:post) .includes(:post)
.order(position: :desc) .order(position: :desc).limit(100)
.limit(limit) .limit(limit)
render json: programmes.map { |programme| render json: programmes.map { |programme|
-1
ファイルの表示
@@ -2,7 +2,6 @@ class TheatreSkipEventsController < ApplicationController
def index def index
limit = params[:limit].to_i limit = params[:limit].to_i
limit = 50 if limit <= 0 limit = 50 if limit <= 0
limit = 100 if limit > 100
events = events =
TheatreSkipEvent TheatreSkipEvent
+5 -6
ファイルの表示
@@ -8,12 +8,11 @@ class WikiPagesController < ApplicationController
title = params[:title].to_s.strip title = params[:title].to_s.strip
descriptions = descriptions =
HistoryAccess.current(TagDescription.all, TagDescriptionVersion) TagDescription.where(language_code: locale.language_code)
.where(language_code: locale.language_code)
.includes(tag: :tag_names) .includes(tag: :tag_names)
descriptions = descriptions.order(updated_at: :desc, id: :asc) descriptions = descriptions.order(updated_at: :desc, id: :asc)
tag_groups = HistoryAccess.current(TagGroup.all, TagGroupVersion).order(updated_at: :desc, id: :asc) tag_groups = TagGroup.order(updated_at: :desc, id: :asc)
if title.present? if title.present?
pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%" pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%"
descriptions = descriptions =
@@ -41,13 +40,13 @@ class WikiPagesController < ApplicationController
return head :not_found unless language.is_a?(String) return head :not_found unless language.is_a?(String)
return head :not_found unless Language.exists?(code: language) return head :not_found unless Language.exists?(code: language)
names = HistoryAccess.current(TagDescription.all, TagDescriptionVersion).joins(tag: :tag_names) names = TagDescription.joins(tag: :tag_names)
.where(language_code: locale.language_code, tag_names: { language_code: language }) .where(language_code: locale.language_code, tag_names: { language_code: language })
.pluck('tag_descriptions.tag_id', 'tag_descriptions.id', 'tag_names.name') .pluck('tag_descriptions.tag_id', 'tag_descriptions.id', 'tag_names.name')
candidates = names.map do |tag_id, description_id, name| candidates = names.map do |tag_id, description_id, name|
WikiPageRepr.autolink_tag(locale, tag_id:, description_id:, name:, language:) WikiPageRepr.autolink_tag(locale, tag_id:, description_id:, name:, language:)
end end
candidates.concat(HistoryAccess.current(TagGroup.all, TagGroupVersion).order(:id).pluck(:id, :name).map do |id, name| candidates.concat(TagGroup.order(:id).pluck(:id, :name).map do |id, name|
WikiPageRepr.autolink_group(locale, id:, name:, language:) WikiPageRepr.autolink_group(locale, id:, name:, language:)
end) end)
render json: candidates render json: candidates
@@ -411,7 +410,7 @@ class WikiPagesController < ApplicationController
end end
def tag_description_by_title locale, title def tag_description_by_title locale, title
HistoryAccess.current(TagDescription.all, TagDescriptionVersion).joins(tag: :tag_names) TagDescription.joins(tag: :tag_names)
.find_by(language_code: locale.language_code, .find_by(language_code: locale.language_code,
tag_names: { language_code: locale.language_code, name: title }) tag_names: { language_code: locale.language_code, name: title })
end end
-23
ファイルの表示
@@ -147,29 +147,6 @@ class HistoryAccess < ActiveSupport::CurrentAttributes
scope.where(Arel.sql(sql(scope.klass, row))) scope.where(Arel.sql(sql(scope.klass, row)))
end end
def self.current(scope, version_model)
return scope if admin?
table = scope.klass.table_name
keys = KEYS.fetch(version_model.table_name)
matches = keys.map do |key|
column = scope.klass.column_names.include?(key) ? key : 'id'
"current_snapshot.#{ key } = #{ table }.#{ column }"
end.join(' AND ')
boundary = "#{ matches } AND current_snapshot.version_no >= #{ table }.version_no " +
"AND current_snapshot.event_type = 'discard'"
scope = scope.where(Arel.sql("NOT EXISTS (SELECT 1 FROM #{ version_model.table_name } " +
"current_snapshot WHERE #{ boundary })"))
condition = "#{ matches } AND current_snapshot.version_no = #{ table }.version_no"
scope.where(Arel.sql("NOT EXISTS (SELECT 1 FROM #{ version_model.table_name } " +
"current_snapshot WHERE #{ condition } AND NOT (#{ sql(version_model, 'current_snapshot') }))"))
end
def self.require_current!(record, version_model)
return if admin?
raise Forbidden unless current(record.class.where(id: record.id), version_model).exists?
end
def self.blob_visible?(blob, seen = []) def self.blob_visible?(blob, seen = [])
return true if admin? return true if admin?
return false if seen.include?(blob.id) return false if seen.include?(blob.id)
-1
ファイルの表示
@@ -44,7 +44,6 @@ module TagGroupRepr
end end
def one locale, group def one locale, group
HistoryAccess.require_current!(group, TagGroupVersion)
many(locale, [group]).first many(locale, [group]).first
end end
-1
ファイルの表示
@@ -5,7 +5,6 @@ module WikiPageRepr
module_function module_function
def tag_description locale, description def tag_description locale, description
HistoryAccess.require_current!(description, TagDescriptionVersion)
tag = description.tag tag = description.tag
{ 'id' => description.id, { 'id' => description.id,
-174
ファイルの表示
@@ -1,174 +0,0 @@
require 'rails_helper'
RSpec.describe 'List API limits', type: :request do
shared_examples 'a capped list' do |default_count, minimum_count, paginated|
def response_rows
body = json
body.is_a?(Array) ? body : body.fetch(collection_key)
end
it 'caps oversized limits at 100 and preserves the 100 boundary' do
get path, params: { locale: 'ja', limit: 100 }
expect(response).to have_http_status(:ok)
expect(response_rows.size).to eq(100)
boundary_rows = response_rows
[101, 10_000].each do |limit|
get path, params: { locale: 'ja', limit: }
expect(response).to have_http_status(:ok)
expect(response_rows).to eq(boundary_rows)
end
end
it 'preserves the default, nonpositive and small positive limits' do
get path, params: { locale: 'ja' }
expect(response).to have_http_status(:ok)
expect(response_rows.size).to eq(default_count)
[0, -1].each do |limit|
get path, params: { locale: 'ja', limit: }
expect(response).to have_http_status(:ok)
expect(response_rows.size).to eq(minimum_count)
end
get path, params: { locale: 'ja', limit: 2 }
expect(response).to have_http_status(:ok)
expect(response_rows.size).to eq(2)
end
if paginated
it 'uses the capped limit for page offsets and preserves total count' do
get path, params: { locale: 'ja', limit: 100 }
first_page = response_rows
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(101)
get path, params: { locale: 'ja', limit: 100, page: 2 }
expect(response).to have_http_status(:ok)
expect(response_rows.size).to eq(1)
second_page = response_rows
expect(first_page & second_page).to be_empty
get path, params: { locale: 'ja', limit: 101, page: 2 }
expect(response).to have_http_status(:ok)
expect(response_rows).to eq(second_page)
expect(json.fetch('count')).to eq(101)
end
end
end
context 'posts' do
let(:path) { '/posts' }
let(:collection_key) { 'posts' }
before { create_list(:post, 101) }
include_examples 'a capped list', 20, 1, true
end
context 'tags' do
let(:path) { '/tags' }
let(:collection_key) { 'tags' }
before { create_list(:tag, 101) }
include_examples 'a capped list', 20, 1, true
end
context 'nico tags' do
let(:path) { '/tags/nico' }
let(:collection_key) { 'tags' }
before { create_list(:external_tag, 101) }
include_examples 'a capped list', 20, 1, true
end
context 'materials' do
let(:path) { '/materials' }
let(:collection_key) { 'materials' }
before do
101.times { |index| Material.create!(url: "https://example.com/material-#{ index }") }
end
include_examples 'a capped list', 20, 1, true
end
context 'post versions' do
let(:path) { '/posts/versions' }
let(:collection_key) { 'versions' }
let(:post) { create(:post) }
before do
101.times do |index|
PostVersion.create!(post:, version_no: index + 1, event_type: 'update',
url: post.url, tags: '', tags_json: [], parent_post_ids: '')
end
end
include_examples 'a capped list', 20, 1, true
end
context 'tag versions' do
let(:path) { '/tags/versions' }
let(:collection_key) { 'versions' }
let(:tag) { create(:tag) }
before do
101.times do |index|
TagVersion.create!(tag:, version_no: index + 1, event_type: 'update',
name: 'limit tag', category: 'general', aliases: '',
parent_tag_ids: '', names: {
ja: { name: 'limit tag', script_code: 'Jpan',
auto_generated: false, aliases: [] } })
end
end
include_examples 'a capped list', 20, 1, true
end
context 'material versions' do
let(:path) { '/materials/versions' }
let(:collection_key) { 'versions' }
let(:material) { Material.create!(url: 'https://example.com/material') }
before do
101.times do |index|
MaterialVersion.create!(material:, version_no: index + 1,
event_type: 'update', url: material.url)
end
end
include_examples 'a capped list', 20, 1, true
end
context 'theatre lists' do
let(:theatre) { create(:theatre) }
let(:post) { create(:post) }
context 'comments' do
let(:path) { "/theatres/#{ theatre.id }/comments" }
before { create_list(:theatre_comment, 101, theatre:, user: theatre.created_by_user) }
include_examples 'a capped list', 20, 20, false
end
context 'programmes' do
let(:path) { "/theatres/#{ theatre.id }/programmes" }
before do
101.times do |index|
TheatreProgramme.create!(theatre:, post:, position: index + 1)
end
end
include_examples 'a capped list', 100, 100, false
end
context 'skip events' do
let(:path) { "/theatres/#{ theatre.id }/skip_events" }
before do
101.times do |index|
TheatreSkipEvent.create!(theatre:, post:,
skipped_by_user: theatre.created_by_user,
created_at: Time.utc(2026, 1, 1) + index.seconds)
end
end
include_examples 'a capped list', 50, 50, false
end
end
end