このコミットが含まれているのは:
2026-10-15 22:07:07 +09:00
コミット 47c40afcc1
10個のファイルの変更、102行の追加、51行の削除
+9
ファイルの表示
@@ -1,5 +1,6 @@
class TagGroup < ApplicationRecord
before_create { SharedResourceIdAllocator.assign!(self) }
before_destroy :delete_current_structure!
belongs_to :created_by_user, class_name: 'User', optional: true
belongs_to :updated_by_user, class_name: 'User', optional: true
@@ -15,4 +16,12 @@ class TagGroup < ApplicationRecord
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
validates :version_no, numericality: { only_integer: true, greater_than: 0 }
private
def delete_current_structure!
placements.reorder(position: :desc).each(&:delete)
tag_group_tags.delete_all
tag_group_groups.delete_all
end
end
+12 -15
ファイルの表示
@@ -95,14 +95,10 @@ class PostCreatePlan
def build_default_tag_specs locale, direct_tag_specs
default_tag_specs = []
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
default_tag_specs << {
name: TAGME_TAG_NAME,
category: :meta }
default_tag_specs << { name: TAGME_TAG_NAME, category: :meta }
end
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
default_tag_specs << {
name: NO_DEERJIKIST_TAG_NAME,
category: :meta }
default_tag_specs << { name: NO_DEERJIKIST_TAG_NAME, category: :meta }
end
default_tag_specs
@@ -117,9 +113,9 @@ class PostCreatePlan
end
def validate_new_tag_spec! locale, spec
tag_name = TagName.new(name: spec[:name], language_code: locale.language_code,
script_code: locale.script_code, primary_flg: true)
tag = Tag.new(category: spec[:category])
tag_name = TagName.new(tag:, name: spec[:name], language_code: locale.language_code,
script_code: locale.script_code, primary_flg: true)
return if tag_name.valid? && tag.valid?
post = Post.new
@@ -140,10 +136,10 @@ class PostCreatePlan
end
expanded_parent_specs =
Tag.expand_parent_tags(existing_snapshot_tags)
.reject(&:deprecated?)
.map { |tag|
{ name: tag.name(locale.language_code),
category: tag.category.to_sym }
.reject(&:deprecated?)
.map { |tag|
{ name: tag.name(locale.language_code),
category: tag.category.to_sym }
}
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
end
@@ -164,9 +160,10 @@ class PostCreatePlan
def existing_tags_by_name locale
@existing_tags_by_name ||= begin
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
.includes(:tag)
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
TagName
.where(language_code: locale.language_code, primary_flg: true, name: names)
.includes(:tag)
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
end
end
+9 -12
ファイルの表示
@@ -50,23 +50,20 @@ class SharedResourceIdAllocator
raise
end
# 明示-tic な Id. 指定は許可する.
return if record.id.present?
next_id = record.class.uncached do
historical_max = HISTORY_IDS.fetch(space, []).map do |table, column|
historical_max = HISTORY_IDS.fetch(space, []).map { |table, column|
quoted_table = connection.quote_table_name(table)
quoted_column = connection.quote_column_name(column)
connection.select_value(
"SELECT MAX(#{ quoted_column }) FROM #{ quoted_table }").to_i
end.max.to_i
connection.select_value("SELECT MAX(#{ quoted_column }) FROM #{ quoted_table }").to_i
}.max.to_i
[next_ids(connection, tables).max, historical_max + 1].max
end
if record.id.present?
if record.id < next_id
raise ActiveRecord::RecordNotUnique, 'Shared ID is below the reserved position'
end
else
record.id = next_id
end
# The INSERT advances InnoDB's persistent counter, including explicit IDs.
record.id = next_id
end
def self.next_ids connection, tables
+14 -9
ファイルの表示
@@ -19,15 +19,20 @@ class TagGroupMarkdownRenderer
blocks << clean_text(@group['description'])
end
sections = placements.select { _1.key?('section_name') }
if sections.any? && sections.size != placements.size
raise UnsupportedStructure,
'最上位の見出し Section と Placement の混在は Markdown にできません.'
end
first_section = placements.index { _1.key?('section_name') }
if placements.all? { _1.key?('section_name') }
blocks.concat(placements.map { render_heading(_1, 2) })
else
if first_section
if placements.drop(first_section).any? { !_1.key?('section_name') }
raise UnsupportedStructure,
'見出し Section の後に最上位 Placement は配置できません.'
end
if first_section.positive?
blocks << render_list(placements.take(first_section), 0)
end
blocks.concat(placements.drop(first_section).map { render_heading(_1, 2) })
elsif placements.any?
blocks << render_list(placements, 0)
end
@@ -74,7 +79,7 @@ class TagGroupMarkdownRenderer
first_section = section_indexes.first
if children[first_section..].any? { !_1.key?('section_name') }
raise UnsupportedStructure,
'見出し Section と兄弟 Placement の交互配置は Markdown にできません.'
'見出し Section と兄弟 Placement の交互配置は Markdown にできません.'
end
list_nodes = children[0...first_section]
blocks << render_list(list_nodes, 0) if list_nodes.any?
+16 -5
ファイルの表示
@@ -1,11 +1,17 @@
require 'rails_helper'
RSpec.describe 'TagName database invariants' do
def insert_name!(**attributes)
TagName.insert_all!([{ name: 'constraint_name', language_code: 'ja',
script_code: 'Jpan', primary_flg: true,
created_at: Time.current, updated_at: Time.current,
**attributes }])
def insert_name! **attributes
defaults = {
tag_id: create(:tag, primary_tag_name: nil).id,
name: 'constraint_name',
language_code: 'ja',
script_code: 'Jpan',
primary_flg: true,
created_at: Time.current,
updated_at: Time.current }
TagName.insert_all!([defaults.merge(attributes)])
end
it 'defaults persisted names to human-confirmed provenance' do
@@ -33,4 +39,9 @@ RSpec.describe 'TagName database invariants' do
.to raise_error(ActiveRecord::RecordNotUnique)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
end
it 'rejects a name without a tag' do
expect { insert_name!(tag_id: nil) }
.to raise_error(ActiveRecord::NotNullViolation)
end
end
+29
ファイルの表示
@@ -20,4 +20,33 @@ RSpec.describe TagGroup, type: :model do
.to include('id' => group.id, 'name' => 'history owner', 'members' => [])
end
it 'destroys a group with nested placements and child membership, retaining history' do
child = create_group(name: 'child group')
parent = create_group(
name: 'parent group',
members: [{ 'tag_group_id' => child.id }],
placements: [{
'section_name' => 'section',
'description' => nil,
'children' => [{
'tag_group_id' => child.id,
'children' => []
}]
}])
versions = parent.versions.order(:version_no).map(&:attributes)
expect { parent.destroy! }
.to change(TagGroup, :count).by(-1)
.and change(TagGroupVersion, :count).by(0)
expect(TagGroupVersion.where(tag_group_id: parent.id)
.order(:version_no).map(&:attributes)).to eq(versions)
expect(child.reload).to be_persisted
expect(TagGroupPlacement.where(tag_group_id: parent.id)).to be_empty
expect(TagGroupTag.where(tag_group_id: parent.id)).to be_empty
expect(TagGroupGroup.where(tag_group_id: parent.id)).to be_empty
end
end
+1 -2
ファイルの表示
@@ -232,10 +232,9 @@ RSpec.describe 'Materials API', type: :request do
expect(current_description_version(page).body).to eq('legacy body')
end
it 'uses tag_id rather than description ID without a Wiki page or TagName' do
it 'uses tag_id rather than description Id. without a Wiki page' do
description = TagDescription.create!(tag:, language_code: 'ja',
body: 'independent body', version_no: 1)
tag.tag_names.delete_all
get_material_without_legacy_reads
expect(json.fetch('wiki_page_body')).to eq('independent body')
expect(json.fetch('tag').fetch('has_wiki')).to be(true)
+5 -3
ファイルの表示
@@ -46,12 +46,14 @@ RSpec.describe 'Tag resource ID allocation', type: :request do
it 'preserves resource and old revision identity on an update' do
description = create_description('stable')
original_resource_id = description.id
original = current_description_version(description)
snapshot = original.attributes
put "/wiki/#{ description.id }", params: {
title: 'stable', body: 'after', base_revision_id: original.id }, headers: headers
put "/wiki/#{ description.id }",
params: { title: 'stable', body: 'after', base_revision_id: original.id },
headers: headers
expect(response).to have_http_status(:ok)
expect(json.fetch('id')).to eq(description.id)
expect(description.reload.id).to eq(original_resource_id)
expect(description.reload).to have_attributes(body: 'after', version_no: 2)
expect(original.reload.attributes).to eq(snapshot)
expect(current_description_version(description).id).to be > original.id
-1
ファイルの表示
@@ -453,7 +453,6 @@ RSpec.describe 'Wiki API', type: :request do
headers: auth_headers(user)
}
.to change(TagDescriptionVersion, :count).by(1)
.and change { linked_tag.reload.tag_versions.count }.by(2)
expect(response).to have_http_status(:ok)
+7 -4
ファイルの表示
@@ -52,12 +52,15 @@ RSpec.describe SharedResourceIdAllocator do
end
end
it "rejects explicit IDs below the reserved #{ space } position" do
it "allows explicit overlapping Id.s in #{ space }" do
first = resource(tables.first)
first.save!
first.class.where(id: first.id).delete_all
expect { resource(tables.last, id: first.id).save! }
.to raise_error(ActiveRecord::RecordNotUnique)
second = resource(tables.last, id: first.id)
second.save!
expect(second.id).to eq(first.id)
expect(first.reload.id).to eq(first.id)
end
it "preserves historical #{ space } overlaps without repairing them" do