コミットを比較

..
作成者 SHA1 メッセージ 日付
Codex Night Worker 63ea2727f0 WIP: preserve Issue #479 changes 2026-11-15 15:40:04 +09:00
54個のファイルの変更、204行の追加、414行の削除
+1 -1
ファイルの表示
@@ -424,7 +424,7 @@ races according to the target constraint's business meaning.
and JSON keys are generally snake_case.
- Preserve existing HTTP status conventions:
`:unauthorized` for no user, `:forbidden` for insufficient role or banned
user, `:not_found` for missing records, and `:unprocessable_entity` for
user, `:not_found` for missing records, and `:unprocessable_content` for
validation failures.
- For diagnostic or internal helper JSON, prefer a deliberately light response
shape over full representation classes when callers only need identifiers,
+1 -1
ファイルの表示
@@ -95,7 +95,7 @@ class ApplicationController < ActionController::API
render_validation_error base: ['すでに存在してゐます.']
end
def render_validation_error record = nil, fields: { }, base: [], status: :unprocessable_entity
def render_validation_error record = nil, fields: { }, base: [], status: :unprocessable_content
errors = { }
if record
+3 -3
ファイルの表示
@@ -18,7 +18,7 @@ class CountsExportsController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue ActiveRecord::StatementTimeout
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
@@ -63,7 +63,7 @@ class CountsExportsController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue ActiveRecord::StatementTimeout
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
@@ -107,7 +107,7 @@ class CountsExportsController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue ActiveRecord::StatementTimeout
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
+1 -1
ファイルの表示
@@ -24,7 +24,7 @@ class CountsTablesController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue CountsPivotTotals::TimedOut
Rails.logger.warn('counts_query_timeout: pivot_totals')
render json: { error: 'counts_query_timeout', stage: 'totals',
+2 -2
ファイルの表示
@@ -15,7 +15,7 @@ class GekanatorGamesController < ApplicationController
answers:)
if game.invalid?
render json: { errors: game.errors.full_messages }, status: :unprocessable_entity
render json: { errors: game.errors.full_messages }, status: :unprocessable_content
return
end
@@ -31,7 +31,7 @@ class GekanatorGamesController < ApplicationController
learned_example_count:
}, status: :created
rescue ActiveRecord::RecordInvalid => e
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_content
end
def extra_questions
+1 -1
ファイルの表示
@@ -536,7 +536,7 @@ class MaterialsController < ApplicationController
errors: { },
base_errors: ['ZIP export に必要な素材ファイルが欠損しています.'],
missing_files: },
status: :unprocessable_entity
status: :unprocessable_content
end
def build_uploaded_material_blob! file, file_sha256
+2 -2
ファイルの表示
@@ -429,7 +429,7 @@ class PostsController < ApplicationController
filters = literals.map do |literal|
internal, = source_ids.fetch(literal[:name])
name = internal.empty? ? literal[:name] : names[internal.first]
return head :unprocessable_entity unless name && internal.length <= 1
return head :unprocessable_content unless name && internal.length <= 1
literal[:negative] ? "not:#{ name }" : name
end
@@ -438,7 +438,7 @@ class PostsController < ApplicationController
literals.zip(translated).each do |literal, translation|
unless (source_ids.fetch(literal[:name]).map(&:sort) ==
target_ids.fetch(translation[:name]).map(&:sort))
return head :unprocessable_entity
return head :unprocessable_content
end
end
index = 0
+1 -64
ファイルの表示
@@ -1,34 +1,4 @@
class TagDescriptionsController < ApplicationController
rescue_from TagDescriptionOperation::Conflict, with: :render_conflict
def index
locale = resolve_locale!
descriptions = HistoryAccess.current(TagDescription.all, TagDescriptionVersion)
.where(language_code: locale.language_code).includes(tag: :tag_names)
.order(updated_at: :desc, id: :asc)
title = params[:title].to_s.strip
if title.present?
pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%"
descriptions = descriptions.joins(tag: :tag_names)
.where(tag_names: { language_code: locale.language_code, primary_flg: true })
.where('tag_names.name LIKE ?', pattern).limit(20)
end
render json: descriptions.map { WikiPageRepr.tag_description(locale, _1) }
end
def show_by_id
locale = resolve_locale!
reference = TagDescription.find_by(id: params[:description_id])
return head :not_found unless reference
HistoryAccess.require_current!(reference, TagDescriptionVersion)
description = TagDescription.find_by(tag_id: reference.tag_id,
language_code: locale.language_code)
return head :not_found unless description
render json: representation(description)
end
def show
locale = resolve_locale!
description = TagDescription.find_by(tag_id: params[:id], language_code: locale.language_code)
@@ -48,25 +18,6 @@ class TagDescriptionsController < ApplicationController
render json: representation(description)
end
def create
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
title = params[:title].to_s.strip
body = params[:body].to_s
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
return render_unprocessable_entity field: :title if title.include?(':')
tag_name = TagName.find_by(language_code: locale.language_code, name: title)
return render_unprocessable_entity field: :title unless tag_name&.tag
description = TagDescriptionOperation.create!(
locale, tag: tag_name.tag, body:, created_by_user: current_user)
render json: representation(description), status: :created
end
def update
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
@@ -81,9 +32,7 @@ class TagDescriptionsController < ApplicationController
description = TagDescription.find_by(tag:, language_code: locale.language_code)
if description
TagDescriptionOperation.update_with_title!(
locale, tag:, body:, title: params[:title], actor: current_user,
base_revision_id: params[:base_revision_id].presence)
TagDescriptionOperation.update!(locale, tag:, body:, updated_by_user: current_user)
description.reload
render json: representation(description)
else
@@ -99,20 +48,8 @@ class TagDescriptionsController < ApplicationController
private
def render_conflict error
render json: { error: 'conflict', message: error.message }, status: :conflict
end
def representation description
HistoryAccess.require_current!(description, TagDescriptionVersion)
if params[:document] == '1'
locale = resolve_locale!
version = HistoryAccess.visible(TagDescriptionVersion.all).find_by!(
tag_id: description.tag_id, language_code: description.language_code,
version_no: description.version_no)
return WikiPageRepr.tag_description(locale, description).merge(revision_id: version.id)
end
description.as_json(only: [
:tag_id,
:language_code,
+7 -56
ファイルの表示
@@ -1,22 +1,7 @@
class TagGroupsController < ApplicationController
rescue_from TagGroupMarkdownParser::InvalidDocument,
TagGroupMarkdownRenderer::UnsupportedStructure, with: :render_document_error
def index
locale = resolve_locale!
groups = HistoryAccess.current(TagGroup.all, TagGroupVersion)
.includes(:created_by_user, :updated_by_user)
if params[:document] == '1'
groups = groups.order(updated_at: :desc, id: :asc)
title = params[:title].to_s.strip
if title.present?
pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%"
groups = groups.where('CONCAT(?, name) LIKE ? OR CONCAT(?, name) LIKE ?',
TagGroupRepr::PREFIX, pattern, 'tag_group:', pattern).limit(20)
end
return render json: groups.map { WikiPageRepr.tag_group(_1) }
end
groups = groups.order(:id)
groups = HistoryAccess.current(TagGroup.all, TagGroupVersion).includes(:created_by_user, :updated_by_user).order(:id)
render json: TagGroupRepr.many(locale, groups.to_a)
end
@@ -25,15 +10,7 @@ class TagGroupsController < ApplicationController
group = TagGroup.includes(:created_by_user, :updated_by_user).find_by(id: params[:id])
return head :not_found unless group
render json: representation(locale, group)
end
def show_by_name
locale = resolve_locale!
group = TagGroup.includes(:created_by_user, :updated_by_user).find_by(name: params[:name])
return head :not_found unless group
render json: representation(locale, group)
render json: TagGroupRepr.one(locale, group)
end
def create
@@ -41,8 +18,8 @@ class TagGroupsController < ApplicationController
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
group = TagGroupOperation.create!(locale, **write_attributes(locale), actor: current_user)
render json: representation(locale, group), status: :created
group = TagGroupOperation.create!(locale, **write_attributes, actor: current_user)
render json: TagGroupRepr.one(locale, group), status: :created
end
def update
@@ -53,39 +30,13 @@ class TagGroupsController < ApplicationController
group = TagGroup.find_by(id: params[:id])
return head :not_found unless group
TagGroupOperation.update!(locale, group:, **write_attributes(locale, group:), actor: current_user)
render json: representation(locale, group.reload)
TagGroupOperation.update!(locale, group:, **write_attributes, actor: current_user)
render json: TagGroupRepr.one(locale, group.reload)
end
private
def render_document_error error
render_unprocessable_entity(error.message)
end
def representation locale, group
representation = TagGroupRepr.one(locale, group)
return representation unless params[:document] == '1'
version = TagGroupRepr.current_version(group)
raise ActiveRecord::RecordNotFound unless version
TagGroupRepr.document(representation, version)
end
def write_attributes locale, group: nil
if params.key?(:body)
body = params[:body].to_s
if body.blank?
group ||= TagGroup.new
group.errors.add(:body, :blank)
raise ActiveRecord::RecordInvalid, group
end
parsed = TagGroupMarkdownParser.parse(locale, body:, group:)
return parsed.except(:compatibility_body).merge(
name: params[:name], require_compatibility: true)
end
def write_attributes
{ name: params[:name], description: params[:description],
members: params[:members], placements: params[:placements] }
end
+2 -2
ファイルの表示
@@ -55,7 +55,7 @@ class TheatresController < ApplicationController
theatre = Theatre.find_by(id: params[:id])
return head :not_found unless theatre
requested_post_id = params[:post_id].to_i
return head :unprocessable_entity if requested_post_id <= 0
return head :unprocessable_content if requested_post_id <= 0
skipped = false
conflicted = false
@@ -95,7 +95,7 @@ class TheatresController < ApplicationController
theatre = Theatre.find_by(id: params[:id])
return head :not_found unless theatre
requested_post_id = params[:post_id].to_i
return head :unprocessable_entity if requested_post_id <= 0
return head :unprocessable_content if requested_post_id <= 0
conflicted = false
+34 -12
ファイルの表示
@@ -6,11 +6,6 @@ class WikiPagesController < ApplicationController
def index
locale = resolve_locale!
resource_type = params[:resource_type]
unless resource_type.nil? || ['tag_description', 'tag_group'].include?(resource_type)
return head :bad_request
end
title = params[:title].to_s.strip
descriptions =
HistoryAccess.current(TagDescription.all, TagDescriptionVersion)
@@ -19,8 +14,6 @@ class WikiPagesController < ApplicationController
descriptions = descriptions.order(updated_at: :desc, id: :asc)
tag_groups = HistoryAccess.current(TagGroup.all, TagGroupVersion).order(updated_at: :desc, id: :asc)
descriptions = descriptions.none if resource_type == 'tag_group'
tag_groups = tag_groups.none if resource_type == 'tag_description'
if title.present?
pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%"
descriptions =
@@ -253,8 +246,25 @@ class WikiPagesController < ApplicationController
base_revision_id = params[:base_revision_id].presence
TagDescriptionOperation.update_with_title!(
locale, tag:, body:, title:, actor: current_user, base_revision_id:)
ApplicationRecord.transaction do
SharedResourceIdAllocator.lock_space!(Tag, 'tags')
tag.lock!
old_title = tag_name.name
if title != old_title
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
end
tag_name.update!(name: title) if title != old_title
message = params[:message].presence
TagDescriptionOperation.update!(locale, tag:, body:, updated_by_user: current_user,
message:, base_revision_id:)
if title != old_title
tag.reload
TagVersionRecorder.record!(tag:, event_type: :update, created_by_user: current_user)
end
end
head :ok
rescue ActiveRecord::RecordInvalid => e
@@ -316,9 +326,21 @@ class WikiPagesController < ApplicationController
end
def render_group_representation representation, version, body: nil, status: :ok
document = TagGroupRepr.document(representation, version)
document['body'] = body if body
render json: document, status:
body ||= TagGroupMarkdownRenderer.render(representation)
versions = HistoryAccess.visible(TagGroupVersion.all).where(tag_group_id: version.tag_group_id)
pred = versions.find_by(version_no: version.version_no - 1)&.id
succ = versions.find_by(version_no: version.version_no + 1)&.id
updated_at = version.created_at
render json: {
'id' => version.tag_group_id,
'resource_type' => 'tag_group',
'title' => "#{ TagGroupRepr::PREFIX }#{ representation.fetch('name') }",
'deprecated_at' => nil,
'body' => body,
'revision_id' => version.id,
'pred' => pred,
'succ' => succ,
'updated_at' => updated_at }, status:
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
render_unprocessable_entity(error.message)
end
-15
ファイルの表示
@@ -56,21 +56,6 @@ module TagGroupRepr
'updated_at' => wiki_page.updated_at }
end
def document representation, version
HistoryAccess.require!(version)
versions = HistoryAccess.visible(TagGroupVersion.all)
.where(tag_group_id: version.tag_group_id)
{ 'id' => version.tag_group_id,
'resource_type' => 'tag_group',
'title' => "#{ PREFIX }#{ representation.fetch('name') }",
'deprecated_at' => nil,
'body' => TagGroupMarkdownRenderer.render(representation),
'revision_id' => version.id,
'pred' => versions.find_by(version_no: version.version_no - 1)&.id,
'succ' => versions.find_by(version_no: version.version_no + 1)&.id,
'updated_at' => version.created_at }
end
def current_version group
HistoryAccess.visible(TagGroupVersion.all).find_by(tag_group_id: group.id, version_no: group.version_no)
end
-31
ファイルの表示
@@ -37,37 +37,6 @@ class TagDescriptionOperation
end
end
def self.update_with_title! locale, tag:, body:, title:, actor:, base_revision_id: nil
return update!(locale, tag:, body:, updated_by_user: actor,
base_revision_id:) if title.nil?
title = title.to_s.strip
tag_name = tag.tag_name(locale.language_code)
if title.blank? || title.include?(':') || !tag_name || tag_name.name.include?(':')
tag.errors.add(:title, :invalid)
raise ActiveRecord::RecordInvalid, tag
end
ApplicationRecord.transaction do
SharedResourceIdAllocator.lock_space!(Tag, 'tags')
tag.lock!
old_title = tag_name.name
if title != old_title
TagVersioning.ensure_snapshot!(tag, created_by_user: actor)
end
tag_name.update!(name: title) if title != old_title
update!(locale, tag:, body:, updated_by_user: actor,
base_revision_id:)
if title != old_title
tag.reload
TagVersionRecorder.record!(tag:, event_type: :update, created_by_user: actor)
end
end
end
def self.record_version! description:, body:, user:
creating = description.new_record?
version_no = creating ? 1 : description.version_no + 1
+1 -8
ファイルの表示
@@ -17,11 +17,7 @@ Rails.application.routes.draw do
resources :locales, only: [:index]
resources :sitemap_entries, only: [:index, :show], param: :resource
resources :tag_groups, only: [:index, :show, :create, :update] do
collection do
get 'name/:name', action: :show_by_name
end
end
resources :tag_groups, only: [:index, :show, :create, :update]
resources :nico_tags, path: 'tags/nico', only: [:index, :show, :update]
@@ -32,9 +28,6 @@ Rails.application.routes.draw do
resources :tags, only: [:index, :show] do
collection do
get :descriptions, to: 'tag_descriptions#index'
post :descriptions, to: 'tag_descriptions#create'
get 'descriptions/:description_id', to: 'tag_descriptions#show_by_id'
get :autocomplete
get :'with-depth', action: :with_depth
get :versions, to: 'tag_versions#index'
+1 -1
ファイルの表示
@@ -262,7 +262,7 @@ RSpec.describe 'Counts table API', type: :request do
tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 3))
end
get '/counts/query', params: base.merge(mode: 'summary', rows: '', cols: 'title', sort: '')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq('error' => 'too_many_columns', 'max_columns' => 100)
end
+2 -2
ファイルの表示
@@ -20,7 +20,7 @@ RSpec.describe 'error responses', type: :request do
patch "/tags/#{ tag.id }", params: { category: 'nico' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -37,7 +37,7 @@ RSpec.describe 'error responses', type: :request do
put "/users/#{ user.id }", params: { name: 'a' * 256 }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.')
+1 -1
ファイルの表示
@@ -49,7 +49,7 @@ RSpec.describe 'Gekanator games API', type: :request do
question_count: 4,
answers: [{ question_id: 'tag:1', answer: 'no' }] }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'returns unauthorized without a user' do
+4 -4
ファイルの表示
@@ -126,7 +126,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
}
}.not_to change { GekanatorGame.count }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'stores a game result for a non-admin user' do
@@ -785,7 +785,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
}
}.not_to change { GekanatorQuestionExample.count }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects non-accepted questions' do
@@ -805,7 +805,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
]
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects non-post_similarity questions' do
@@ -829,7 +829,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
]
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'allows a non-admin user to answer extra questions for their own game' do
+2 -2
ファイルの表示
@@ -55,7 +55,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
deprecated: false, aliases: 'other_primary', parent_tags: '' }
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(other_primary.reload).to have_attributes(tag_id: other_tag.id, primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'ja', primary_flg: true))
.to contain_exactly(other_primary)
@@ -289,7 +289,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
aliases: 'other_ainu_primary', parent_tags: '' }
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(other_primary.reload).to have_attributes(
tag_id: other_tag.id, language_code: 'ain', primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'ain', primary_flg: true))
+6 -6
ファイルの表示
@@ -289,7 +289,7 @@ RSpec.describe 'Materials API', type: :request do
it 'returns 422 when tag is blank' do
post '/materials', params: { tag: ' ', file: dummy_upload }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tag' => ['タグは必須です.'])
end
@@ -297,7 +297,7 @@ RSpec.describe 'Materials API', type: :request do
it 'returns 422 when both file and url are blank' do
post '/materials', params: { tag: 'material_create_blank' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'file' => ['ファイルまたは URL は必須です.'],
'url' => ['ファイルまたは URL は必須です.'])
@@ -381,7 +381,7 @@ RSpec.describe 'Materials API', type: :request do
file: dummy_upload
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'persists url-only material' do
@@ -424,7 +424,7 @@ RSpec.describe 'Materials API', type: :request do
}
end.not_to change(Material, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'file' => ['抑止された素材です: copyright_high_risk']
)
@@ -484,7 +484,7 @@ RSpec.describe 'Materials API', type: :request do
file: dummy_upload
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tag' => ['タグは必須です.'])
end
@@ -695,7 +695,7 @@ RSpec.describe 'Materials API', type: :request do
file: dummy_upload(filename: 'blocked.png', body: 'blocked-update')
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(material.reload.file.blob.filename.to_s).to eq('old.png')
end
end
+2 -2
ファイルの表示
@@ -218,7 +218,7 @@ RSpec.describe 'NicoTags', type: :request do
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'nico:linked_ng' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tags' => ['ニコニコ・タグ同士は連携できません.'])
end
@@ -239,7 +239,7 @@ RSpec.describe 'NicoTags', type: :request do
expect([Tag.count, TagName.count, TagVersion.count]).to eq(before_counts)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors').fetch('tags')).to include(
a_string_including('タグ名 “invalid”:', '名前に使用できない文字が含まれてゐます.'))
end
+1 -1
ファイルの表示
@@ -48,7 +48,7 @@ RSpec.describe 'Post filter identity across locales', type: :request do
primary_tag_name_for(tag, 'ja').update_columns(name: 'nico:shared')
get '/posts/tag_filters', params: {
locale: 'ja', target_locale: 'ain-Kana', tags: 'nico:shared' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects removed reference parameters instead of overriding the name' do
+24 -24
ファイルの表示
@@ -1042,7 +1042,7 @@ RSpec.describe 'Posts API', type: :request do
get '/posts/metadata', params: { url: 'file:///etc/passwd' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to have_key('url')
end
end
@@ -1180,7 +1180,7 @@ RSpec.describe 'Posts API', type: :request do
)
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'thumbnail' => ['サムネイル画像の変換に失敗しました.']
)
@@ -1219,7 +1219,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tags' => ['廃止済みタグがあります: deprecated_direct_tag']
)
@@ -1265,7 +1265,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.'
@@ -1333,7 +1333,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'video_ms' => ['タグ区間の終端が動画時間を超えてゐます.']
)
@@ -1444,7 +1444,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'video_ms' => ['タグ区間の開始が動画時間以上です.']
)
@@ -1465,7 +1465,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity), response.body
expect(response).to have_http_status(:unprocessable_content), response.body
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -1485,7 +1485,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'spec_tag', # 既存タグ名を投げる
thumbnail: dummy_upload)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
@@ -1499,7 +1499,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'spec_tag', # 既存タグ名を投げる
thumbnail: dummy_upload)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
@@ -1601,7 +1601,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload }
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1620,7 +1620,7 @@ RSpec.describe 'Posts API', type: :request do
)
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1639,7 +1639,7 @@ RSpec.describe 'Posts API', type: :request do
)
}.not_to change(PostImplication, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1770,7 +1770,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'deprecated_update_tag'
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tags' => ['廃止済みタグは付与できません.']
)
@@ -1789,7 +1789,7 @@ RSpec.describe 'Posts API', type: :request do
title: 'updated title',
tags: 'nico:nico_tag')
expect(response).to have_http_status(:unprocessable_entity), response.body
expect(response).to have_http_status(:unprocessable_content), response.body
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -1896,7 +1896,7 @@ RSpec.describe 'Posts API', type: :request do
title: 'updated title',
tags: 'spec_tag' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1922,7 +1922,7 @@ RSpec.describe 'Posts API', type: :request do
parent_post_ids: 'abc'
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
end
end
@@ -1948,7 +1948,7 @@ RSpec.describe 'Posts API', type: :request do
parent_post_ids: '999999999'
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
end
end
@@ -1964,7 +1964,7 @@ RSpec.describe 'Posts API', type: :request do
parent_post_ids: post_record.id.to_s
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(PostImplication.exists?(
post_id: post_record.id,
@@ -2669,7 +2669,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload)
end.not_to change(PostVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'does not create a version when PUT /posts/:id is invalid' do
@@ -2685,7 +2685,7 @@ RSpec.describe 'Posts API', type: :request do
original_created_before: Time.zone.local(2020, 1, 1, 0, 0, 0).iso8601)
end.not_to change(PostVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects second-bearing original created timestamps on POST /posts' do
@@ -2698,7 +2698,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload,
original_created_from: '2020-01-01T00:00:01Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_from' => ['オリジナルの作成日時は分単位で入力してください.']
)
@@ -2715,7 +2715,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload,
original_created_before: '2020-01-01T00:00:00.123Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_before' => ['オリジナルの作成日時は分単位で入力してください.']
)
@@ -2733,7 +2733,7 @@ RSpec.describe 'Posts API', type: :request do
original_created_from: '2020-01-01T00:00Z',
original_created_before: '2020-01-01T00:00Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_at' => ['オリジナルの作成日時の順番がをかしぃです.']
)
@@ -2767,7 +2767,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'spec_tag',
original_created_from: 'not-a-time')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_from' => ['オリジナルの作成日時の形式が不正です.']
)
+1 -1
ファイルの表示
@@ -64,7 +64,7 @@ RSpec.describe "TagChildren", type: :request do
do_request
}.not_to change(TagImplication, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(TagImplication.where(tag: child, parent_tag: parent)).not_to exist
end
end
+1 -1
ファイルの表示
@@ -303,7 +303,7 @@ RSpec.describe 'Tag description native API', type: :request do
headers
before_state = stored_state
native_write(body: '')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include('type' => 'validation_error',
'errors' => include('body' => a_kind_of(Array)))
expect(stored_state).to eq(before_state)
+2 -2
ファイルの表示
@@ -84,7 +84,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
put "/wiki/#{ group.id }", params: {
locale: 'ain-Kana', title: 'タググループ:partial rename', body: '- japanese_member' },
headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -117,7 +117,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
expect(representation.fetch('members').sole.fetch('tag').fetch('name')).to be_nil
expect(representation.fetch('placements').sole.fetch('tag').fetch('name')).to be_nil
get "/wiki/#{ old_group.id }", params: { locale: 'ain-Kana', version: version.id }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(response.body).not_to include('historical_japanese', 'current_ainu')
end
end
+2 -2
ファイルの表示
@@ -133,7 +133,7 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
get group_title_path('混在構造')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
end
@@ -142,7 +142,7 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
get group_title_path('未配置構造')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
end
end
+1 -1
ファイルの表示
@@ -21,7 +21,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
before_state = group_state
yield
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
expect(response.media_type).to eq('application/json')
expect(response.body).not_to be_empty
+1 -1
ファイルの表示
@@ -27,7 +27,7 @@ RSpec.describe 'Tag group write integrity', type: :request do
def expect_no_writes
before_state = group_state
yield
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
+7 -7
ファイルの表示
@@ -148,7 +148,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(operation, payload.merge(invalid))
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
@@ -158,7 +158,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
invalid = [{ tag_id: tag.id, section_name: '不正', children: [] }]
native_write(operation, payload.merge(placements: invalid))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -167,7 +167,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(operation, payload.merge(
members: [{ tag_id: missing }], placements: [{ tag_id: missing, children: [] }]))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -177,7 +177,7 @@ RSpec.describe 'Tag group native API', type: :request do
native_write(operation, payload.merge(
members: [{ tag_group_id: missing }],
placements: [{ tag_group_id: missing, children: [] }]))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -186,7 +186,7 @@ RSpec.describe 'Tag group native API', type: :request do
6.times { nodes = [{ tag_id: tag.id, children: nodes }] }
before_state = group_state
native_write(operation, payload.merge(placements: nodes))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
@@ -206,7 +206,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(:update, payload.merge(
members: [{ tag_group_id: parent }], placements: []))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -214,7 +214,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(:update, payload.merge(
members: [{ tag_group_id: group_id }], placements: []))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
+3 -3
ファイルの表示
@@ -297,7 +297,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
do_request
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -321,7 +321,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
do_request
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'deerjikists.0.platform' => [be_present],
)
@@ -352,7 +352,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
do_request
}.not_to change { Deerjikist.order(:platform, :code).pluck(:platform, :code, :tag_id) }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'deerjikists.1.code' => [include('existing-deerjikist')],
)
+10 -10
ファイルの表示
@@ -696,7 +696,7 @@ RSpec.describe 'Tags API', type: :request do
it 'nico category への変更は 422 を返す' do
patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -740,7 +740,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{tag.id}", params: { category: 'nico' }
}.not_to change(TagVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(tag.reload.category).to eq('general')
end
@@ -824,7 +824,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{ tag.id }", params: { name: 'unknown' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'name' => ['その名前は既に使はれてゐます.']
)
@@ -840,7 +840,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{ system_tag.id }", params: { name: 'patch_system_tag_renamed' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq('bot操作')
end
end
@@ -1144,7 +1144,7 @@ RSpec.describe 'Tags API', type: :request do
parent_tags: '',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
end
@@ -1156,7 +1156,7 @@ RSpec.describe 'Tags API', type: :request do
parent_tags: '',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -1169,7 +1169,7 @@ RSpec.describe 'Tags API', type: :request do
parent_tags: '',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'deprecated' => ['廃止状態は必須です.']
)
@@ -1366,7 +1366,7 @@ RSpec.describe 'Tags API', type: :request do
}
}.not_to change(TagVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
@@ -1387,7 +1387,7 @@ RSpec.describe 'Tags API', type: :request do
}
}.not_to change(TagVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq(old_name)
expect(system_tag.category).to eq(old_category)
@@ -1569,7 +1569,7 @@ RSpec.describe 'Tags API', type: :request do
deprecated: '0',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(TagImplication.where(tag:, parent_tag: child)).not_to exist
end
+1 -1
ファイルの表示
@@ -131,7 +131,7 @@ RSpec.describe 'TheatreComments', type: :request do
post "/theatres/#{theatre.id}/comments", params: { content: ' ' }
}.not_to change(TheatreComment, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'theatre が存在しなければ 404 を返す' do
+1 -1
ファイルの表示
@@ -376,7 +376,7 @@ RSpec.describe 'Theatres API', type: :request do
put "/theatres/#{theatre.id}/skip_vote", params: { post_id: 'invalid' }
}.not_to change(TheatreSkipVote, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'records a vote and returns the current vote status before majority' do
+2 -2
ファイルの表示
@@ -79,7 +79,7 @@ RSpec.describe 'user settings', type: :request do
patch '/users/settings', params: { theme: 1 }, as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include('type' => 'validation_error')
expect(json.fetch('errors')).to include(
'theme' => ['値の型が不正です.'])
@@ -90,7 +90,7 @@ RSpec.describe 'user settings', type: :request do
patch '/users/settings', params: { theme: 'neon' }, as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include('type' => 'validation_error')
expect(json.fetch('errors').fetch('theme')).to be_present
end
+2 -2
ファイルの表示
@@ -84,7 +84,7 @@ RSpec.describe 'user theme slots', type: :request do
params: { tokens: 'not-object' },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'base_theme' => ['値が不正です.'])
end
@@ -96,7 +96,7 @@ RSpec.describe 'user theme slots', type: :request do
params: { tokens: 'not-object' },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tokens' => ['JSON object で指定してください.'])
end
+1 -1
ファイルの表示
@@ -119,7 +119,7 @@ RSpec.describe 'Users', type: :request do
params: { name: ' ' },
headers: auth_headers(user)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'name' => ['名前は必須です.'])
end
+1 -1
ファイルの表示
@@ -39,7 +39,7 @@ RSpec.describe 'Wiki creation freeze', type: :request do
it "rejects invalid description body #{ body.inspect } atomically" do
before_state = group_state
put "/wiki/#{ page.id }", params: { title: tag.name('ja'), body: }, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
+3 -3
ファイルの表示
@@ -100,12 +100,12 @@ RSpec.describe 'Wiki API', type: :request do
context 'when params invalid' do
it 'returns 422 when title blank' do
post endpoint, params: { title: '', body: 'Hello' }, headers: auth_headers(member)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'returns 422 when body blank' do
post endpoint, params: { title: 'Test', body: '' }, headers: auth_headers(member)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
@@ -160,7 +160,7 @@ RSpec.describe 'Wiki API', type: :request do
put "/wiki/#{page.id}",
params: { title: 'TestPage', body: '' },
headers: auth_headers(member)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
+1 -1
ファイルの表示
@@ -34,7 +34,7 @@ RSpec.describe 'Wiki title collision', type: :request do
},
headers: auth_headers(user)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
source.reload
+1 -1
ファイルの表示
@@ -18,7 +18,7 @@ RSpec.describe 'Removed standalone Wiki resources', type: :request do
before_state = group_state
post '/wiki', params: { title:, body: 'body' },
headers: cookie_headers(user)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
+1 -6
ファイルの表示
@@ -166,12 +166,7 @@ export const pageRoutes = (
robots: 'noindex', authentication: 'user' } },
{ path: ':id', element: <MaterialDetailPage/>,
handle: { preload: MaterialDetailPage.preload, sitemap: ['materials'] } }] },
{ path: 'wiki', element: <LocaleRedirect to="/tags"/>,
handle: { redirectTo: '/tags' } },
{ path: 'tag-descriptions', element: <LocaleRedirect to="/tags"/>,
handle: { redirectTo: '/tags' } },
{ path: 'tag-groups',
element: <WikiSearchPage key="tag_group" resourceType="tag_group"/>,
{ path: 'wiki', element: <WikiSearchPage/>,
handle: { preload: WikiSearchPage.preload, sitemap: ['static'] } },
{ path: 'wiki/:title', element: <WikiDetailPage/>,
handle: { preload: WikiDetailPage.preload, switchLocale: switchWikiLocale,
+14 -28
ファイルの表示
@@ -29,7 +29,7 @@ import { fetchWikiPage } from '@/lib/wiki'
import type { FC, MouseEvent } from 'react'
import type { AvailableLocale, LocaleCode } from '@/lib/locales'
import type { Material, Menu, MenuVisibleItem, Tag, User, WikiPage } from '@/types'
import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types'
type Props = {
user: User | null
@@ -39,11 +39,10 @@ type NavDensity = 'auto' | 'comfortable' | 'compact'
export const menuOutline = (
{ tag, material, wikiId, wikiResourceType, user, pathName: fullPathname }: {
{ tag, material, wikiId, user, pathName: fullPathname }: {
tag?: Tag | null
material?: Material | null
wikiId: number | null
wikiResourceType?: WikiPage['resourceType']
user: User | null
pathName: string },
): Menu => {
@@ -100,29 +99,17 @@ export const menuOutline = (
visible: materialFlg && Boolean (material?.tag) },
{ ...message ('history'), to: `/materials/changes?material_id=${ material?.id }`,
visible: materialFlg }] },
...([
{ key: 'tagGroups', path: '/tag-groups', type: 'tag_group' }] as const)
.map (({ key, path, type }): MenuVisibleItem => {
const current = (wikiResourceType ?? 'tag_description') === type
const detailFlg = wikiPageFlg && current
const creationPath =
type === 'tag_group'
? `/wiki/new?title=${ encodeURIComponent ('タググループ:') }` : '/wiki/new'
return { ...message (key), to: path,
base: (pathName.startsWith ('/wiki') && current) ? '/wiki' : path,
subMenu: [
{ ...message ('search'), to: path },
{ ...message ('create'), to: creationPath, visible: editable },
{ ...message ('allHistory'), to: '/wiki/changes' },
{ ...message ('help'), to: '/wiki/ヘルプ:Wiki' },
{ component: <Separator/>, visible: detailFlg },
{ ...message ('postsWithCount', { count: postCount }),
to: `/posts?tags=${ wikiTitle }`, visible: detailFlg },
{ ...message ('history'), to: `/wiki/changes?id=${ wikiId }`,
visible: detailFlg },
{ ...message ('edit'), to: `/wiki/${ wikiId || wikiTitle }/edit`,
visible: detailFlg && editable }] }
}),
{ ...message ('wiki'), to: '/wiki', subMenu: [
{ ...message ('search'), to: '/wiki' },
{ ...message ('create'), to: '/wiki/new', visible: editable },
{ ...message ('allHistory'), to: '/wiki/changes' },
{ ...message ('help'), to: '/wiki/ヘルプ:Wiki' },
{ component: <Separator/>, visible: wikiPageFlg },
{ ...message ('postsWithCount', { count: postCount }), to: `/posts?tags=${ wikiTitle }`,
visible: wikiPageFlg },
{ ...message ('history'), to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ ...message ('edit'), to: `/wiki/${ wikiId || wikiTitle }/edit`,
visible: wikiPageFlg && editable }] },
{ ...message ('help'), to: '/help/home', base: '/help', subMenu: [
{ ...message ('search'), to: '/help' },
{ ...message ('allHistory'), to: '/help/changes' },
@@ -242,8 +229,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
queryKey: materialsKeys.show (effectiveTitle, locale),
queryFn: () => fetchMaterial (effectiveTitle, locale) })
const menu = menuOutline ({ tag, material, wikiId,
wikiResourceType: wikiPage?.resourceType, user, pathName: location.pathname })
const menu = menuOutline ({ tag, material, wikiId, user, pathName: location.pathname })
const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
const menuLabels = visibleMenu.map (item => item.name).join ('\u0000')
const moreMenu = menu.filter (item =>
+3 -5
ファイルの表示
@@ -100,11 +100,10 @@ const prefetchHelpDiff: Prefetcher = async (qc, url, locale) => {
const prefetchWikiPagesIndex: Prefetcher = async (qc, url, locale) => {
const title = url.searchParams.get ('title') ?? ''
const resourceType = 'tag_group'
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ title, resourceType }, locale),
queryFn: () => fetchWikiPages ({ title, resourceType }, locale) })
queryKey: wikiKeys.index ({ title }, locale),
queryFn: () => fetchWikiPages ({ title }, locale) })
}
@@ -311,8 +310,7 @@ export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[]
&& Boolean (mPost (u.pathname))),
run: prefetchPostShow },
{ test: u => u.pathname === '/posts/changes', run: prefetchPostChanges },
{ test: u => u.pathname === '/tag-groups',
run: prefetchWikiPagesIndex },
{ test: u => u.pathname === '/wiki', run: prefetchWikiPagesIndex },
{ test: u => (!(['/wiki/new', '/wiki/changes'].includes (u.pathname))
&& Boolean (mWiki (u.pathname))),
run: prefetchWikiPageShow },
+1 -2
ファイルの表示
@@ -5,7 +5,6 @@ import type {
FetchPostsParams,
FetchTagsParams,
MaterialFilter,
WikiPage,
WikiTagReference } from '@/types'
export const localesKeys = {
@@ -131,7 +130,7 @@ export const wikiKeys = {
autolinkCandidates: (language: string, locale: LocaleCode) =>
['wiki', 'autolink', locale, language] as const,
index: (
p: { title?: string, resourceType?: WikiPage['resourceType'] },
p: { title?: string },
locale: LocaleCode = 'ja',
) => ['wiki', locale, 'index', p] as const,
show: (
+12 -56
ファイルの表示
@@ -1,6 +1,6 @@
import { isAxiosError } from 'axios'
import { apiGet, apiPost, apiPut } from '@/lib/api'
import { apiGet } from '@/lib/api'
import { switchLocalePath } from '@/lib/locales'
import { fetchTag, fetchTagByName } from '@/lib/tags'
@@ -24,54 +24,19 @@ export const fetchWikiChanges = (
export const fetchWikiPages = async (
{ title, resourceType = 'tag_description' }: {
title?: string, resourceType?: WikiPage['resourceType'] },
{ title }: { title?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage[]> => apiGet (
resourceType === 'tag_group' ? '/tag_groups' : '/tags/descriptions',
{ locale, params: { title, document: '1' } })
): Promise<WikiPage[]> =>
await apiGet ('/wiki', { locale, params: { title } })
export const fetchWikiPage = async (
id: string,
{ version }: { version?: string },
id: string,
{ version }: { version?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage> => {
if (version != null)
return apiGet (`/wiki/${encodeURIComponent (id)}`, { locale, params: { version } })
try
{
return await apiGet (`/tags/descriptions/${encodeURIComponent (id)}`,
{ locale, params: { document: '1' } })
}
catch (error)
{
if (!isAxiosError (error) || error.response?.status !== 404)
throw error
return apiGet (`/tag_groups/${encodeURIComponent (id)}`,
{ locale, params: { document: '1' } })
}
}
export const saveTagDocument = async (
{ title, body, page }: { title: string, body: string, page?: WikiPage },
locale: LocaleCode,
): Promise<WikiPage> => {
const groupName = /^(?:タググループ:|tag_group:)(.*)$/i.exec (title.trim ())?.[1]
const options = { locale, params: { document: '1' } }
if (page?.resourceType === 'tag_group' || (page == null && groupName != null))
{
const attributes = { name: groupName ?? title.trim (), body }
return page == null
? apiPost ('/tag_groups', attributes, options)
: apiPut (`/tag_groups/${encodeURIComponent (String (page.id))}`, attributes, options)
}
if (page == null)
return apiPost ('/tags/descriptions', { title, body }, options)
return apiPut (`/tags/${encodeURIComponent (String (page.tagId))}/description`,
{ title, body }, options)
}
): Promise<WikiPage> =>
await apiGet (`/wiki/${ encodeURIComponent (id) }`, {
locale, params: version ? { version } : { } })
export const fetchWikiPageByTitle = async (
@@ -81,14 +46,7 @@ export const fetchWikiPageByTitle = async (
): Promise<WikiPage | null> => {
try
{
if (version != null)
return await apiGet (`/wiki/title/${encodeURIComponent (title)}`,
{ locale, params: { version } })
const groupName = /^(?:タググループ:|tag_group:)(.*)$/i.exec (title.trim ())?.[1]
const path = groupName == null
? `/tags/name/${encodeURIComponent (title)}/description`
: `/tag_groups/name/${encodeURIComponent (groupName)}`
return await apiGet (path, { locale, params: { document: '1' } })
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { locale, params: { version } })
}
catch (error)
{
@@ -114,7 +72,7 @@ export const fetchWikiPageByReference = async (
{
if (!isAxiosError (error) || error.response?.status !== 404)
throw error
const tag = await fetchTagByName (reference, locale)
const tag = await fetchWikiTag ({ wikiId: reference }, locale)
return tag == null ? null : fetchWikiPageByTitle (tag.name, params, locale)
}
}
@@ -128,9 +86,7 @@ export const fetchWikiTag = async (
return fetchTagByName (reference.name, locale)
try
{
const page = await fetchWikiPage (reference.wikiId, { }, locale)
return (page.resourceType === 'tag_description' && page.tagId != null)
? fetchTag (String (page.tagId), locale) : null
return await apiGet (`/wiki/${encodeURIComponent (reference.wikiId)}/tag`, { locale })
}
catch (error)
{
-2
ファイルの表示
@@ -14,8 +14,6 @@
"materials": "チカㇻペ",
"suppressions": "アエアイカㇷ゚テㇷ゚",
"wiki": "Wiki",
"tagDescriptions": "$t(counts:resources.tag_descriptions)",
"tagGroups": "$t(counts:resources.tag_groups)",
"create": "アシㇼ",
"edit": "イヌイェㇷ゚",
"entertainment": "シノッ",
-2
ファイルの表示
@@ -13,8 +13,6 @@
"materials": "素材",
"suppressions": "抑止",
"wiki": "Wiki",
"tagDescriptions": "$t(counts:resources.tag_descriptions)",
"tagGroups": "$t(counts:resources.tag_groups)",
"create": "新規",
"edit": "編輯",
"entertainment": "おたのしみ",
+1 -1
ファイルの表示
@@ -135,7 +135,7 @@ const MaterialDetailPage: FC = () => {
<TabGroup>
{material.tag && (
<Tab name={label ('navigation:tagDescriptions')}>
<Tab name={label ('navigation:wiki')}>
<WikiBody
title={material.tag.name}
language={localeLanguage (locale)}
+1 -1
ファイルの表示
@@ -176,7 +176,7 @@ const PostListPage: FC = () => {
{loading && label ('common:loading')}
</Tab>
{tags.length === 1 && (
<Tab name={label ('navigation:tagDescriptions')}>
<Tab name={label ('navigation:wiki')}>
<WikiBody title={tags[0]} body={wikiPage?.body}
language={wikiPage?.languageCode ?? (wikiPage?.resourceType === 'tag_description'
? localeLanguage (locale) : 'und')}/>
+1 -2
ファイルの表示
@@ -51,8 +51,7 @@ const WikiDetailPage: FC = () => {
queryKey: helpKeys.titles ('ja'),
queryFn: () => getHelpTitleIndex ('ja'),
staleTime: Infinity })
const lookupTitle = title.trim ().normalize ('NFKC').toLowerCase ()
.replace (/^(?:ヘルプ|手引):/, '')
const lookupTitle = title.trim ().normalize ('NFKC').toLowerCase ().replace (/^ヘルプ:/, '')
const identifiers = useMemo (() => (
(titleIndex != null && Object.hasOwn (titleIndex, lookupTitle))
? titleIndex[lookupTitle] : []), [titleIndex, lookupTitle])
+13 -10
ファイルの表示
@@ -11,8 +11,8 @@ import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
import PageHead from '@/components/common/PageHead'
import { fetchWikiPage, saveTagDocument } from '@/lib/wiki'
import { tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { apiGet, apiPut } from '@/lib/api'
import { wikiKeys } from '@/lib/queryKeys'
import { canEditContent } from '@/lib/users'
import { inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
@@ -33,7 +33,7 @@ const FIXED_WIKI_EDITOR_VIEW = { menu: true, md: true, html: true } as const
type Props = { user: User | null }
type WikiFormField = 'title' | 'name' | 'body'
type WikiFormField = 'title' | 'body'
const WikiEditPage: FC<Props> = ({ user }) => {
@@ -50,7 +50,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
const source = `${locale}:${id}`
const { data: wiki, isPending: loading } = useQuery ({
queryKey: wikiKeys.show (id ?? '', { }, locale),
queryFn: () => fetchWikiPage (id ?? '', { }, locale),
queryFn: () => apiGet<WikiPage> (`/wiki/${ encodeURIComponent (id ?? '') }`, { locale }),
enabled: editable })
const [body, setBody] = useState (wiki?.body ?? '')
const [loadedSource, setLoadedSource] = useState (wiki == null ? '' : source)
@@ -60,19 +60,22 @@ const WikiEditPage: FC<Props> = ({ user }) => {
useValidationErrors<WikiFormField> ()
const handleSubmit = async () => {
if (pending || !editable || wiki == null)
if (pending || !editable)
return
clearValidationErrors ()
const formData = new FormData ()
formData.append ('title', title)
formData.append ('body', body)
try
{
await saveTagDocument ({ title, body, page: wiki }, locale)
await apiPut (`/wiki/${ id }`, formData,
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (title, { }, locale),
(prev: WikiPage | undefined) =>
prev == null ? prev : { ...prev, title, body })
await Promise.all ([
qc.invalidateQueries ({ queryKey: wikiKeys.root }),
qc.invalidateQueries ({ queryKey: tagsKeys.root })])
qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ title: label ('shared:messages.postSuccess') })
navigate (`/wiki/${ encodeURIComponent (title) }`)
}
@@ -107,7 +110,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
{/* タイトル */}
{/* TODO: タグ補完 */}
<FormField label={label ('shared:labels.title')} messages={fieldErrors.title ?? fieldErrors.name}>
<FormField label={label ('shared:labels.title')} messages={fieldErrors.title}>
{({ describedBy, invalid }) => (
<input type="text"
value={title}
+10 -5
ファイルの表示
@@ -12,7 +12,7 @@ import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
import PageHead from '@/components/common/PageHead'
import { saveTagDocument } from '@/lib/wiki'
import { apiPost } from '@/lib/api'
import { tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { canEditContent } from '@/lib/users'
import { inputClass } from '@/lib/utils'
@@ -21,7 +21,7 @@ import Forbidden from '@/pages/Forbidden'
import type { FC } from 'react'
import type { User } from '@/types'
import type { User, WikiPage } from '@/types'
const mdParser = new MarkdownIt
const FIXED_WIKI_EDITOR_VIEW = { menu: true, md: true, html: true } as const
@@ -32,7 +32,7 @@ const FIXED_WIKI_EDITOR_VIEW = { menu: true, md: true, html: true } as const
type Props = { user: User | null }
type WikiFormField = 'title' | 'name' | 'body'
type WikiFormField = 'title' | 'body'
const WikiNewPage: FC<Props> = ({ user }) => {
@@ -55,9 +55,14 @@ const WikiNewPage: FC<Props> = ({ user }) => {
const handleSubmit = async () => {
clearValidationErrors ()
const formData = new FormData
formData.append ('title', title)
formData.append ('body', body)
try
{
const data = await saveTagDocument ({ title, body }, locale)
const data = await apiPost<WikiPage> ('/wiki', formData,
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
await qc.cancelQueries ({ queryKey: wikiKeys.root })
qc.setQueryData (wikiKeys.show (data.title, { }, locale), data)
await Promise.all ([
@@ -86,7 +91,7 @@ const WikiNewPage: FC<Props> = ({ user }) => {
{/* タイトル */}
{/* TODO: タグ補完 */}
<FormField label={label ('shared:labels.title')} messages={fieldErrors.title ?? fieldErrors.name}>
<FormField label={label ('shared:labels.title')} messages={fieldErrors.title}>
{({ describedBy, invalid }) => (
<input type="text"
value={title}
+7 -11
ファイルの表示
@@ -8,7 +8,7 @@ import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PageHead from '@/components/common/PageHead'
import { fetchWikiPages } from '@/lib/wiki'
import { apiGet } from '@/lib/api'
import { wikiKeys } from '@/lib/queryKeys'
import { dateString, inputClass } from '@/lib/utils'
@@ -17,20 +17,16 @@ import type { FormEvent , FC } from 'react'
import type { WikiPage } from '@/types'
const WikiSearchPage: FC<{ resourceType?: WikiPage['resourceType'] }> = (
{ resourceType = 'tag_description' },
) => {
const WikiSearchPage: FC = () => {
const { message, label } = useUITranslation ('shared')
const locale = useLocale ()
const heading =
resourceType === 'tag_group'
? 'navigation:tagGroups' : 'navigation:tagDescriptions'
const [title, setTitle] = useState ('')
const [text, setText] = useState ('')
const [submittedTitle, setSubmittedTitle] = useState ('')
const { data: results = [], refetch } = useQuery ({
queryKey: wikiKeys.index ({ title: submittedTitle, resourceType }, locale),
queryFn: () => fetchWikiPages ({ title: submittedTitle, resourceType }, locale) })
queryKey: wikiKeys.index ({ title: submittedTitle }, locale),
queryFn: () => apiGet<WikiPage[]> ('/wiki', {
locale, params: { title: submittedTitle } }) })
const handleSearch = (ev: FormEvent) => {
ev.preventDefault ()
@@ -46,10 +42,10 @@ const WikiSearchPage: FC<{ resourceType?: WikiPage['resourceType'] }> = (
return (
<MainArea>
<PageHead title={message (heading)}/>
<PageHead title={message ('navigation:wiki')}/>
<div className="max-w-xl">
<PageTitle>{label (heading)}</PageTitle>
<PageTitle>{label ('navigation:wiki')}</PageTitle>
<form onSubmit={handleSearch} className="space-y-2">
{/* タイトル */}
<FormField label={label ('labels.title')}>