このコミットが含まれているのは:
@@ -1,5 +1,6 @@
|
|||||||
class TagGroup < ApplicationRecord
|
class TagGroup < ApplicationRecord
|
||||||
before_create { SharedResourceIdAllocator.assign!(self) }
|
before_create { SharedResourceIdAllocator.assign!(self) }
|
||||||
|
before_destroy :delete_current_structure!
|
||||||
|
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||||
belongs_to :updated_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 :name, presence: true, uniqueness: true, length: { maximum: 255 }
|
||||||
validates :version_no, numericality: { only_integer: true, greater_than: 0 }
|
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
|
end
|
||||||
|
|||||||
@@ -95,14 +95,10 @@ class PostCreatePlan
|
|||||||
def build_default_tag_specs locale, direct_tag_specs
|
def build_default_tag_specs locale, direct_tag_specs
|
||||||
default_tag_specs = []
|
default_tag_specs = []
|
||||||
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
||||||
default_tag_specs << {
|
default_tag_specs << { name: TAGME_TAG_NAME, category: :meta }
|
||||||
name: TAGME_TAG_NAME,
|
|
||||||
category: :meta }
|
|
||||||
end
|
end
|
||||||
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
|
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
|
||||||
default_tag_specs << {
|
default_tag_specs << { name: NO_DEERJIKIST_TAG_NAME, category: :meta }
|
||||||
name: NO_DEERJIKIST_TAG_NAME,
|
|
||||||
category: :meta }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
default_tag_specs
|
default_tag_specs
|
||||||
@@ -117,9 +113,9 @@ class PostCreatePlan
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_new_tag_spec! locale, spec
|
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 = 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?
|
return if tag_name.valid? && tag.valid?
|
||||||
|
|
||||||
post = Post.new
|
post = Post.new
|
||||||
@@ -140,10 +136,10 @@ class PostCreatePlan
|
|||||||
end
|
end
|
||||||
expanded_parent_specs =
|
expanded_parent_specs =
|
||||||
Tag.expand_parent_tags(existing_snapshot_tags)
|
Tag.expand_parent_tags(existing_snapshot_tags)
|
||||||
.reject(&:deprecated?)
|
.reject(&:deprecated?)
|
||||||
.map { |tag|
|
.map { |tag|
|
||||||
{ name: tag.name(locale.language_code),
|
{ name: tag.name(locale.language_code),
|
||||||
category: tag.category.to_sym }
|
category: tag.category.to_sym }
|
||||||
}
|
}
|
||||||
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
||||||
end
|
end
|
||||||
@@ -164,9 +160,10 @@ class PostCreatePlan
|
|||||||
def existing_tags_by_name locale
|
def existing_tags_by_name locale
|
||||||
@existing_tags_by_name ||= begin
|
@existing_tags_by_name ||= begin
|
||||||
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
|
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
|
||||||
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
|
TagName
|
||||||
.includes(:tag)
|
.where(language_code: locale.language_code, primary_flg: true, name: names)
|
||||||
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
|
.includes(:tag)
|
||||||
|
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -50,23 +50,20 @@ class SharedResourceIdAllocator
|
|||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# 明示-tic な Id. 指定は許可する.
|
||||||
|
return if record.id.present?
|
||||||
|
|
||||||
next_id = record.class.uncached do
|
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_table = connection.quote_table_name(table)
|
||||||
quoted_column = connection.quote_column_name(column)
|
quoted_column = connection.quote_column_name(column)
|
||||||
connection.select_value(
|
connection.select_value("SELECT MAX(#{ quoted_column }) FROM #{ quoted_table }").to_i
|
||||||
"SELECT MAX(#{ quoted_column }) FROM #{ quoted_table }").to_i
|
}.max.to_i
|
||||||
end.max.to_i
|
|
||||||
[next_ids(connection, tables).max, historical_max + 1].max
|
[next_ids(connection, tables).max, historical_max + 1].max
|
||||||
end
|
end
|
||||||
if record.id.present?
|
|
||||||
if record.id < next_id
|
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.
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.next_ids connection, tables
|
def self.next_ids connection, tables
|
||||||
|
|||||||
@@ -19,15 +19,20 @@ class TagGroupMarkdownRenderer
|
|||||||
blocks << clean_text(@group['description'])
|
blocks << clean_text(@group['description'])
|
||||||
end
|
end
|
||||||
|
|
||||||
sections = placements.select { _1.key?('section_name') }
|
first_section = placements.index { _1.key?('section_name') }
|
||||||
if sections.any? && sections.size != placements.size
|
|
||||||
raise UnsupportedStructure,
|
|
||||||
'最上位の見出し Section と Placement の混在は Markdown にできません.'
|
|
||||||
end
|
|
||||||
|
|
||||||
if placements.all? { _1.key?('section_name') }
|
if first_section
|
||||||
blocks.concat(placements.map { render_heading(_1, 2) })
|
if placements.drop(first_section).any? { !_1.key?('section_name') }
|
||||||
else
|
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)
|
blocks << render_list(placements, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -74,7 +79,7 @@ class TagGroupMarkdownRenderer
|
|||||||
first_section = section_indexes.first
|
first_section = section_indexes.first
|
||||||
if children[first_section..].any? { !_1.key?('section_name') }
|
if children[first_section..].any? { !_1.key?('section_name') }
|
||||||
raise UnsupportedStructure,
|
raise UnsupportedStructure,
|
||||||
'見出し Section と兄弟 Placement の交互配置は Markdown にできません.'
|
'見出し Section と兄弟 Placement の交互配置は Markdown にできません.'
|
||||||
end
|
end
|
||||||
list_nodes = children[0...first_section]
|
list_nodes = children[0...first_section]
|
||||||
blocks << render_list(list_nodes, 0) if list_nodes.any?
|
blocks << render_list(list_nodes, 0) if list_nodes.any?
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'TagName database invariants' do
|
RSpec.describe 'TagName database invariants' do
|
||||||
def insert_name!(**attributes)
|
def insert_name! **attributes
|
||||||
TagName.insert_all!([{ name: 'constraint_name', language_code: 'ja',
|
defaults = {
|
||||||
script_code: 'Jpan', primary_flg: true,
|
tag_id: create(:tag, primary_tag_name: nil).id,
|
||||||
created_at: Time.current, updated_at: Time.current,
|
name: 'constraint_name',
|
||||||
**attributes }])
|
language_code: 'ja',
|
||||||
|
script_code: 'Jpan',
|
||||||
|
primary_flg: true,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current }
|
||||||
|
|
||||||
|
TagName.insert_all!([defaults.merge(attributes)])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'defaults persisted names to human-confirmed provenance' do
|
it 'defaults persisted names to human-confirmed provenance' do
|
||||||
@@ -33,4 +39,9 @@ RSpec.describe 'TagName database invariants' do
|
|||||||
.to raise_error(ActiveRecord::RecordNotUnique)
|
.to raise_error(ActiveRecord::RecordNotUnique)
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'rejects a name without a tag' do
|
||||||
|
expect { insert_name!(tag_id: nil) }
|
||||||
|
.to raise_error(ActiveRecord::NotNullViolation)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,4 +20,33 @@ RSpec.describe TagGroup, type: :model do
|
|||||||
.to include('id' => group.id, 'name' => 'history owner', 'members' => [])
|
.to include('id' => group.id, 'name' => 'history owner', 'members' => [])
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -232,10 +232,9 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
expect(current_description_version(page).body).to eq('legacy body')
|
expect(current_description_version(page).body).to eq('legacy body')
|
||||||
end
|
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',
|
description = TagDescription.create!(tag:, language_code: 'ja',
|
||||||
body: 'independent body', version_no: 1)
|
body: 'independent body', version_no: 1)
|
||||||
tag.tag_names.delete_all
|
|
||||||
get_material_without_legacy_reads
|
get_material_without_legacy_reads
|
||||||
expect(json.fetch('wiki_page_body')).to eq('independent body')
|
expect(json.fetch('wiki_page_body')).to eq('independent body')
|
||||||
expect(json.fetch('tag').fetch('has_wiki')).to be(true)
|
expect(json.fetch('tag').fetch('has_wiki')).to be(true)
|
||||||
|
|||||||
@@ -46,12 +46,14 @@ RSpec.describe 'Tag resource ID allocation', type: :request do
|
|||||||
|
|
||||||
it 'preserves resource and old revision identity on an update' do
|
it 'preserves resource and old revision identity on an update' do
|
||||||
description = create_description('stable')
|
description = create_description('stable')
|
||||||
|
original_resource_id = description.id
|
||||||
original = current_description_version(description)
|
original = current_description_version(description)
|
||||||
snapshot = original.attributes
|
snapshot = original.attributes
|
||||||
put "/wiki/#{ description.id }", params: {
|
put "/wiki/#{ description.id }",
|
||||||
title: 'stable', body: 'after', base_revision_id: original.id }, headers: headers
|
params: { title: 'stable', body: 'after', base_revision_id: original.id },
|
||||||
|
headers: headers
|
||||||
expect(response).to have_http_status(:ok)
|
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(description.reload).to have_attributes(body: 'after', version_no: 2)
|
||||||
expect(original.reload.attributes).to eq(snapshot)
|
expect(original.reload.attributes).to eq(snapshot)
|
||||||
expect(current_description_version(description).id).to be > original.id
|
expect(current_description_version(description).id).to be > original.id
|
||||||
|
|||||||
@@ -453,7 +453,6 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
headers: auth_headers(user)
|
headers: auth_headers(user)
|
||||||
}
|
}
|
||||||
.to change(TagDescriptionVersion, :count).by(1)
|
.to change(TagDescriptionVersion, :count).by(1)
|
||||||
|
|
||||||
.and change { linked_tag.reload.tag_versions.count }.by(2)
|
.and change { linked_tag.reload.tag_versions.count }.by(2)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|||||||
@@ -52,12 +52,15 @@ RSpec.describe SharedResourceIdAllocator do
|
|||||||
end
|
end
|
||||||
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 = resource(tables.first)
|
||||||
first.save!
|
first.save!
|
||||||
first.class.where(id: first.id).delete_all
|
|
||||||
expect { resource(tables.last, id: first.id).save! }
|
second = resource(tables.last, id: first.id)
|
||||||
.to raise_error(ActiveRecord::RecordNotUnique)
|
second.save!
|
||||||
|
|
||||||
|
expect(second.id).to eq(first.id)
|
||||||
|
expect(first.reload.id).to eq(first.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "preserves historical #{ space } overlaps without repairing them" do
|
it "preserves historical #{ space } overlaps without repairing them" do
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする