Reviewed-on: #421 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #421 でマージされました。
このコミットが含まれているのは:
@@ -0,0 +1,32 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Language, type: :model do
|
||||
it 'accepts Nipponese as the current supported language' do
|
||||
expect(described_class.find('ja')).to be_valid
|
||||
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['ja'])
|
||||
end
|
||||
|
||||
it 'accepts English when multilingual support is enabled' do
|
||||
pending '日本語以外の Language 有効化は別課題で実装する'
|
||||
expect { described_class.create!(code: 'en', name: 'English') }
|
||||
.to change(described_class, :count).by(1)
|
||||
end
|
||||
|
||||
it 'rejects an unsupported language code' do
|
||||
expect {
|
||||
described_class.create!(code: 'xx', name: 'Unsupported')
|
||||
}.to raise_error { |error|
|
||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
||||
}
|
||||
end
|
||||
|
||||
['en', 'fr'].each do |code|
|
||||
it "does not register #{ code } as a currently supported language" do
|
||||
expect {
|
||||
described_class.create!(code:, name: 'Unsupported')
|
||||
}.to raise_error { |error|
|
||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,129 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Locale, type: :model do
|
||||
include_context 'English locale'
|
||||
|
||||
def create_english_locale!
|
||||
prepare_english_reference!
|
||||
described_class.create!(code: 'en', language_code: 'en',
|
||||
script_code: 'Latn', region_code: 826,
|
||||
variants: '', name: 'English')
|
||||
end
|
||||
|
||||
it 'keeps the active Nipponese locale consistent with its identity tuple' do
|
||||
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['ja'])
|
||||
expect(described_class.nipponese).to have_attributes(
|
||||
code: 'ja', language_code: 'ja', script_code: 'Jpan',
|
||||
region_code: 392, variants: '')
|
||||
end
|
||||
|
||||
it 'rejects a duplicate locale identity tuple at the database boundary' do
|
||||
expect {
|
||||
described_class.insert_all!([{ code: 'ja-duplicate', language_code: 'ja',
|
||||
script_code: 'Jpan', region_code: 392,
|
||||
variants: '', name: '日本語',
|
||||
created_at: Time.current }])
|
||||
}.to raise_error(ActiveRecord::RecordNotUnique)
|
||||
end
|
||||
|
||||
it 'generates a primary name for every existing tag using the name generator' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
first = create(:tag, primary_name: 'first_existing_tag')
|
||||
second = create(:tag, primary_name: 'second_existing_tag')
|
||||
allow(TagName).to receive(:generate_name).and_call_original
|
||||
allow(TagName).to receive(:generate_name)
|
||||
.with(kind_of(described_class), first, primary_tag_name_for(first, 'ja'))
|
||||
.and_return('name_from_generator')
|
||||
|
||||
locale = create_english_locale!
|
||||
|
||||
expect(TagName).to have_received(:generate_name)
|
||||
.with(locale, first, primary_tag_name_for(first, 'ja'))
|
||||
expect(TagName).to have_received(:generate_name)
|
||||
.with(locale, second, primary_tag_name_for(second, 'ja'))
|
||||
expect(first.tag_names.find_by!(language_code: 'en', primary_flg: true))
|
||||
.to have_attributes(name: 'name_from_generator', tag_id: first.id,
|
||||
language_code: 'en', script_code: 'Latn',
|
||||
primary_flg: true, auto_generated: true)
|
||||
expect(second.tag_names.find_by!(language_code: 'en', primary_flg: true))
|
||||
.to have_attributes(tag_id: second.id, language_code: 'en',
|
||||
script_code: 'Latn', primary_flg: true,
|
||||
auto_generated: true)
|
||||
expect(TagName.where(language_code: 'en', primary_flg: true,
|
||||
tag_id: [first.id, second.id]).count).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not duplicate an existing primary name in the new language' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_reference!
|
||||
tag = create(:tag, primary_name: 'already_named')
|
||||
existing = create(:tag_name, name: 'existing_english', tag:,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
create_english_locale!
|
||||
|
||||
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true))
|
||||
.to contain_exactly(existing)
|
||||
expect(existing.reload).to have_attributes(
|
||||
tag_id: tag.id, language_code: 'en', script_code: 'Latn',
|
||||
primary_flg: true, auto_generated: false)
|
||||
end
|
||||
|
||||
it 'creates a primary name when the language has only an alias' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_reference!
|
||||
tag = create(:tag, primary_name: 'alias_only_tag')
|
||||
alias_name = create(:tag_name, :alias, name: 'alias_en',
|
||||
tag:,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
create_english_locale!
|
||||
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: tag.id,
|
||||
language_code: 'en', script_code: 'Latn', primary_flg: false)
|
||||
generated = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
|
||||
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'en',
|
||||
script_code: 'Latn', primary_flg: true,
|
||||
auto_generated: true)
|
||||
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true).count).to eq(1)
|
||||
end
|
||||
|
||||
it 'rolls back the locale and all generated names when backfill fails' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_reference!
|
||||
first = create(:tag, primary_name: 'atomic_first')
|
||||
second = create(:tag, primary_name: 'atomic_second')
|
||||
allow(TagName).to receive(:generate_name).and_call_original
|
||||
allow(TagName).to receive(:generate_name)
|
||||
.with(kind_of(described_class), second, primary_tag_name_for(second, 'ja'))
|
||||
.and_raise('backfill failed')
|
||||
|
||||
expect { create_english_locale! }.to raise_error('backfill failed')
|
||||
|
||||
expect(described_class.exists?(code: 'en')).to be(false)
|
||||
expect(TagName.where(language_code: 'en', tag_id: [first.id, second.id])).to be_empty
|
||||
end
|
||||
|
||||
it 'accepts a stable locale code outside the historical enum values' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_reference!
|
||||
|
||||
locale = described_class.create!(
|
||||
code: 'en-x-rspec', language_code: 'en',
|
||||
script_code: 'Latn', region_code: 826,
|
||||
variants: 'x-rspec', name: 'English')
|
||||
|
||||
expect(locale.reload).to have_attributes(
|
||||
code: 'en-x-rspec', language_code: 'en', script_code: 'Latn',
|
||||
region_code: 826, variants: 'x-rspec')
|
||||
end
|
||||
|
||||
it 'registers the future British English locale tuple' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
|
||||
expect(locale).to have_attributes(code: 'en', language_code: 'en',
|
||||
script_code: 'Latn', region_code: 826,
|
||||
variants: '')
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe MaterialExportItem, type: :model do
|
||||
let(:user) { create(:user, :member) }
|
||||
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'export_item'), category: :material) }
|
||||
let(:tag) { create(:tag, primary_name: 'export_item', category: :material) }
|
||||
let(:material) do
|
||||
Material.create!(tag:, url: 'https://example.com/material',
|
||||
created_by_user: user, updated_by_user: user)
|
||||
|
||||
@@ -18,7 +18,7 @@ RSpec.describe Post, type: :model do
|
||||
expect(post.snapshot_tags_json).to eq([
|
||||
{ 'tag_id' => tag.id,
|
||||
'version_no' => tag.version_no,
|
||||
'name' => tag.name,
|
||||
'name' => primary_tag_name_for(tag, 'ja').name,
|
||||
'category' => tag.category,
|
||||
'sections' => [{ 'begin_ms' => 2000, 'end_ms' => nil }] },
|
||||
{ 'external_tag_id' => external.id }])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PostVersion, type: :model do
|
||||
let!(:tag_name) { TagName.create!(name: 'post_version_spec_tag') }
|
||||
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
|
||||
let!(:tag_name) { create(:tag_name, name: 'post_version_spec_tag') }
|
||||
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
|
||||
|
||||
let!(:post_record) do
|
||||
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Script, type: :model do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'selects locales by script code, not language code' do
|
||||
expect(described_class.find('Jpan').locales).to contain_exactly(Locale.nipponese)
|
||||
end
|
||||
|
||||
it 'accepts supported script codes and rejects unsupported ones' do
|
||||
expect(described_class.find('Jpan')).to be_valid
|
||||
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['Jpan'])
|
||||
expect {
|
||||
described_class.create!(code: 'Zzzz', name: 'Unsupported')
|
||||
}.to raise_error { |error|
|
||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not register Latin as a currently supported script' do
|
||||
expect {
|
||||
described_class.create!(code: 'Latn', name: 'Latin')
|
||||
}.to raise_error { |error|
|
||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
||||
}
|
||||
end
|
||||
|
||||
it 'selects Latin locales when multilingual support is enabled' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
|
||||
expect(described_class.find('Latn').locales).to contain_exactly(locale)
|
||||
expect(described_class.find('Jpan').locales).not_to include(locale)
|
||||
end
|
||||
end
|
||||
@@ -2,8 +2,8 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe TagImplication, type: :model do
|
||||
it 'rejects a parent tag that would create a cycle' do
|
||||
child = create(:tag, name: 'tag_implication_cycle_child')
|
||||
parent = create(:tag, name: 'tag_implication_cycle_parent')
|
||||
child = create(:tag, primary_name: 'tag_implication_cycle_child')
|
||||
parent = create(:tag, primary_name: 'tag_implication_cycle_parent')
|
||||
|
||||
described_class.create!(tag: child, parent_tag: parent)
|
||||
|
||||
@@ -17,9 +17,9 @@ RSpec.describe TagImplication, type: :model do
|
||||
end
|
||||
|
||||
it 'terminates even when existing data already contains a cycle' do
|
||||
child = create(:tag, name: 'tag_implication_existing_cycle_child')
|
||||
parent = create(:tag, name: 'tag_implication_existing_cycle_parent')
|
||||
ancestor = create(:tag, name: 'tag_implication_existing_cycle_ancestor')
|
||||
child = create(:tag, primary_name: 'tag_implication_existing_cycle_child')
|
||||
parent = create(:tag, primary_name: 'tag_implication_existing_cycle_parent')
|
||||
ancestor = create(:tag, primary_name: 'tag_implication_existing_cycle_ancestor')
|
||||
|
||||
described_class.create!(tag: parent, parent_tag: ancestor)
|
||||
described_class.insert_all!(
|
||||
|
||||
@@ -31,12 +31,48 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
|
||||
describe '.apply!' do
|
||||
before do
|
||||
described_class.create!(priority: 10, source_pattern: '_', replacement: '')
|
||||
described_class.create!(priority: 10, source_pattern: 'foo_', replacement: 'foo')
|
||||
end
|
||||
|
||||
context 'when only another language has the sanitised name' do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'keeps both names when sanitisation produces a cross-language match' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
nipponese = create(:tag_name, name: 'foobar')
|
||||
english = create(:tag_name, name: 'temporary',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english.update_columns(name: 'foo_bar')
|
||||
|
||||
expect { described_class.apply! }.not_to change(TagName, :count)
|
||||
|
||||
expect(english.reload).to have_attributes(name: 'foobar', language_code: 'en')
|
||||
expect(nipponese.reload).to have_attributes(name: 'foobar', language_code: 'ja')
|
||||
end
|
||||
|
||||
it 'renames without merging tags or deleting either language identity' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
nipponese = create(:tag, primary_name: 'foobar')
|
||||
english_name = create(:tag_name, name: 'temporary',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name)
|
||||
english_name.update_columns(name: 'foo_bar')
|
||||
|
||||
described_class.apply!
|
||||
|
||||
expect(english_name.reload).to have_attributes(
|
||||
name: 'foobar', language_code: 'en', tag_id: english.id)
|
||||
expect(primary_tag_name_for(nipponese.reload, 'ja')).to have_attributes(
|
||||
name: 'foobar', language_code: 'ja', tag_id: nipponese.id)
|
||||
expect(primary_tag_name_for(english.reload, 'en')).to eq(english_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no conflicting tag_name exists' do
|
||||
let!(:tag_name) do
|
||||
TagName.create!(name: 'tmp').tap do |tn|
|
||||
create(:tag_name, name: 'tmp').tap do |tn|
|
||||
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
@@ -47,10 +83,10 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a conflicting canonical tag_name exists' do
|
||||
let!(:existing) { TagName.create!(name: 'foobar') }
|
||||
context 'when a conflicting primary tag name exists' do
|
||||
let!(:existing) { create(:tag_name, name: 'foobar') }
|
||||
let!(:source) do
|
||||
TagName.create!(name: 'tmp').tap do |tn|
|
||||
create(:tag_name, name: 'tmp').tap do |tn|
|
||||
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
@@ -63,48 +99,56 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
end
|
||||
|
||||
context 'when the source tag_name has a tag and the existing one has no tag' do
|
||||
let!(:existing) { TagName.create!(name: 'foobar') }
|
||||
let!(:source_tag) { create(:tag, name: 'tmp', category: :general) }
|
||||
let!(:source_tag_name_id) { source_tag.tag_name_id }
|
||||
let!(:existing) { create(:tag_name, name: 'foobar') }
|
||||
let!(:source_tag) { create(:tag, primary_name: 'tmp', category: :general) }
|
||||
let!(:source_tag_name_id) { primary_tag_name_for(source_tag, 'ja').id }
|
||||
|
||||
before do
|
||||
source_tag.tag_name.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||
primary_tag_name_for(source_tag, 'ja').update_columns(
|
||||
name: 'foo_bar', updated_at: Time.current)
|
||||
end
|
||||
|
||||
it 'moves the tag to the existing tag_name' do
|
||||
described_class.apply!
|
||||
expected_tag_name_id = existing.canonical_id || existing.id
|
||||
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
|
||||
expect(existing.reload).to have_attributes(
|
||||
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
|
||||
expect(primary_tag_name_for(source_tag, 'ja')).to eq(existing)
|
||||
expect(primary_tag_name_for(source_tag.reload, 'ja')).to eq(existing)
|
||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the sanitised name is an alias of an existing tag' do
|
||||
let!(:existing_tag) { create(:tag) }
|
||||
let!(:existing_primary) { primary_tag_name_for(existing_tag, 'ja') }
|
||||
let!(:alias_name) do
|
||||
TagName.create!(name: 'foobar', canonical: existing_tag.tag_name)
|
||||
create(:tag_name, :alias, name: 'foobar', tag: existing_tag)
|
||||
end
|
||||
let!(:source) do
|
||||
TagName.create!(name: 'tmp').tap do |tn|
|
||||
create(:tag_name, name: 'tmp').tap do |tn|
|
||||
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
it 'deletes only the source and preserves the alias and its canonical tag' do
|
||||
it 'deletes only the source and preserves the alias and its owning tag' do
|
||||
described_class.apply!
|
||||
|
||||
expect(TagName.unscoped.exists?(source.id)).to be(false)
|
||||
expect(alias_name.reload.canonical).to eq(existing_tag.tag_name)
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: existing_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(TagName.find_by!(tag_id: existing_tag.id,
|
||||
language_code: 'ja', primary_flg: true))
|
||||
.to eq(existing_primary)
|
||||
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when both source and existing tag_names have tags' do
|
||||
let!(:existing_tn) { TagName.create!(name: 'foobar') }
|
||||
let!(:existing_tag) { Tag.create!(tag_name: existing_tn, category: :general) }
|
||||
let!(:existing_tn) { create(:tag_name, name: 'foobar') }
|
||||
let!(:existing_tag) { create(:tag, primary_tag_name: existing_tn, category: :general) }
|
||||
|
||||
let!(:source_tn) { TagName.create!(name: 'tmp') }
|
||||
let!(:source_tag) { Tag.create!(tag_name: source_tn, category: :general) }
|
||||
let!(:source_tn) { create(:tag_name, name: 'tmp') }
|
||||
let!(:source_tag) { create(:tag, primary_tag_name: source_tn, category: :general) }
|
||||
let!(:source_tag_name_id) { source_tn.id }
|
||||
|
||||
before do
|
||||
@@ -120,7 +164,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||
expect(post.reload.tags).to contain_exactly(existing_tag)
|
||||
expect(existing_tag.reload.name).to eq('foobar')
|
||||
expect(primary_tag_name_for(existing_tag.reload, 'ja').name).to eq('foobar')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagName, type: :model do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'allows the same name in different languages' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
create(:tag_name, name: 'shared_name')
|
||||
english = build(:tag_name, name: 'shared_name',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect(english).to be_valid
|
||||
expect { english.save! }.to change(described_class, :count).by(1)
|
||||
end
|
||||
|
||||
it 'rejects a duplicate name within the same language' do
|
||||
create(:tag_name, name: 'shared_name')
|
||||
duplicate = build(:tag_name, name: 'shared_name')
|
||||
|
||||
expect(duplicate).to be_invalid
|
||||
expect(duplicate.errors.of_kind?(:name, :taken)).to be(true)
|
||||
end
|
||||
|
||||
it 'creates Nipponese factory names without requiring a tag' do
|
||||
name = create(:tag_name)
|
||||
|
||||
expect(name.reload).to have_attributes(
|
||||
language_code: 'ja', script_code: 'Jpan', primary_flg: true,
|
||||
auto_generated: false, tag_id: nil)
|
||||
end
|
||||
|
||||
it 'defaults ordinary persisted human names to confirmed provenance' do
|
||||
name = described_class.create!(name: 'ordinary_human_name', language_code: 'ja',
|
||||
script_code: 'Jpan', primary_flg: true)
|
||||
|
||||
expect(name.reload.auto_generated).to be(false)
|
||||
end
|
||||
|
||||
it 'persists both sides of the representative tag factory association' do
|
||||
name = create(:tag_name)
|
||||
tag = create(:tag, primary_tag_name: name)
|
||||
|
||||
expect(name.reload).to have_attributes(
|
||||
tag_id: tag.id, language_code: 'ja', script_code: 'Jpan', primary_flg: true)
|
||||
expect(name.tag).to eq(tag)
|
||||
expect(described_class.find_by!(tag_id: tag.id, language_code: 'ja', primary_flg: true))
|
||||
.to eq(name)
|
||||
expect(tag.reload.tag_name_id).to eq(name.id)
|
||||
end
|
||||
|
||||
it 'creates a complete ordinary tag factory under the active Nipponese language' do
|
||||
tag = create(:tag, primary_name: 'factory_multilingual')
|
||||
|
||||
Locale.where(deprecated_at: nil).distinct.pluck(:language_code).each do |language_code|
|
||||
expect(TagName.where(tag:, language_code:, primary_flg: true).count).to eq(1)
|
||||
end
|
||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
|
||||
.to contain_exactly(have_attributes(auto_generated: false))
|
||||
end
|
||||
|
||||
it 'completes an ordinary tag factory under future active languages' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
tag = create(:tag, primary_name: 'future_factory')
|
||||
|
||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
|
||||
.to contain_exactly(have_attributes(auto_generated: true))
|
||||
end
|
||||
|
||||
it 'builds a primary name without persisting the tag or its name' do
|
||||
tag = nil
|
||||
|
||||
expect {
|
||||
tag = build(:tag, primary_name: 'built_primary')
|
||||
}.to change(Tag, :count).by(0).and change(described_class, :count).by(0)
|
||||
|
||||
expect(tag).to be_new_record
|
||||
expect(tag.tag_names.target).to contain_exactly(have_attributes(
|
||||
name: 'built_primary', language_code: 'ja', script_code: 'Jpan',
|
||||
primary_flg: true))
|
||||
end
|
||||
|
||||
it 'persists an alias owned by the same tag as its primary name' do
|
||||
primary_name = create(:tag_name)
|
||||
tag = create(:tag, primary_tag_name: primary_name)
|
||||
alias_name = create(:tag_name, :alias, name: 'valid_alias', tag:)
|
||||
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: tag.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
primary = described_class.find_by!(tag_id: alias_name.tag_id,
|
||||
language_code: alias_name.language_code,
|
||||
primary_flg: true)
|
||||
expect(primary).to eq(primary_name)
|
||||
end
|
||||
|
||||
describe '.canonicalise' do
|
||||
it 'leaves detached aliases unchanged' do
|
||||
create(:tag_name, :alias, name: 'detached_alias')
|
||||
|
||||
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
|
||||
.to eq(['detached_alias'])
|
||||
end
|
||||
|
||||
it 'does not resolve a detached alias through another detached primary' do
|
||||
create(:tag_name, :alias, name: 'detached_alias')
|
||||
create(:tag_name, name: 'unrelated_detached_primary')
|
||||
|
||||
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
|
||||
.to eq(['detached_alias'])
|
||||
end
|
||||
|
||||
it 'resolves the primary name of the alias language on the same tag' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
tag = create(:tag, primary_name: '日本語正本')
|
||||
english_primary = primary_tag_name_for(tag, 'en')
|
||||
english_primary.update!(name: 'english_primary', auto_generated: false)
|
||||
english_alias = create(:tag_name, :alias, tag:, name: 'english_alias',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
representative_id = tag.tag_name_id
|
||||
|
||||
primary = described_class.find_by!(tag_id: english_alias.tag_id,
|
||||
language_code: english_alias.language_code,
|
||||
primary_flg: true)
|
||||
|
||||
expect(primary).to eq(english_primary)
|
||||
expect(described_class.canonicalise(locale, [english_alias.name]))
|
||||
.to eq([english_primary.name])
|
||||
expect(tag.reload.tag_name_id).to eq(representative_id)
|
||||
end
|
||||
|
||||
it 'resolves only aliases in the requested language' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
nipponese = create(:tag, primary_name: 'nipponese_canonical')
|
||||
english_name = create(:tag_name, name: 'english_canonical',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name)
|
||||
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: nipponese)
|
||||
english_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: english,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect(nipponese_alias.reload).to have_attributes(
|
||||
tag_id: nipponese.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
expect(english_alias.reload).to have_attributes(
|
||||
tag_id: english.id,
|
||||
primary_flg: false, language_code: 'en')
|
||||
expect(described_class.canonicalise(locale, ['shared_alias']))
|
||||
.to eq(['english_canonical'])
|
||||
expect(described_class.canonicalise(Locale.nipponese, ['shared_alias']))
|
||||
.to eq(['nipponese_canonical'])
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects the generated-name prefix in human input' do
|
||||
human_name = build(:tag_name, name: 'Tag_#999', auto_generated: false)
|
||||
|
||||
expect(human_name).to be_invalid
|
||||
end
|
||||
|
||||
it 'allows the generated-name prefix for system-created names' do
|
||||
generated_name = build(:tag_name, name: 'Tag_#1000', auto_generated: true)
|
||||
|
||||
expect { generated_name.save! }.to change(described_class, :count).by(1)
|
||||
end
|
||||
end
|
||||
+352
-96
@@ -4,12 +4,12 @@ RSpec.describe Tag, type: :model do
|
||||
describe 'external tag separation' do
|
||||
['nico:reserved', 'NiCo:reserved'].each do |name|
|
||||
it "rejects the reserved prefix #{ name } for internal tags" do
|
||||
tag = build(:tag, name:)
|
||||
tag = build(:tag, primary_name: name)
|
||||
|
||||
expect(tag).to be_invalid
|
||||
expect(tag.errors[:name]).to be_present
|
||||
expect {
|
||||
described_class.normalise_tags!([name])
|
||||
described_class.normalise_tags!(Locale.nipponese, [name])
|
||||
}.to raise_error(Tag::NicoTagNormalisationError)
|
||||
end
|
||||
end
|
||||
@@ -25,128 +25,161 @@ RSpec.describe Tag, type: :model do
|
||||
end
|
||||
|
||||
describe '.normalise_tags!' do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'canonicalises aliases in the supplied language only' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_reference!
|
||||
nipponese = create(:tag, primary_name: 'nipponese_canonical')
|
||||
english_name = create(:tag_name, name: 'english_canonical',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name)
|
||||
primary_tag_name_for(english, 'ja').update!(
|
||||
name: 'english_in_nipponese', auto_generated: false)
|
||||
english_locale = locale
|
||||
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: nipponese)
|
||||
english_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: english,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect(nipponese_alias.reload).to have_attributes(
|
||||
tag_id: nipponese.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
expect(english_alias.reload).to have_attributes(
|
||||
tag_id: english.id,
|
||||
primary_flg: false, language_code: 'en')
|
||||
expect(described_class.normalise_tags!(
|
||||
english_locale, ['shared_alias'],
|
||||
with_tagme: false, with_no_deerjikist: false)).to eq([english])
|
||||
expect(described_class.normalise_tags!(
|
||||
Locale.nipponese, ['shared_alias'],
|
||||
with_tagme: false, with_no_deerjikist: false)).to eq([nipponese])
|
||||
end
|
||||
|
||||
it 'rejects deprecated tags when deny_deprecated is enabled' do
|
||||
tag_name = TagName.create!(name: 'normalise deprecated tag')
|
||||
deprecated_tag = Tag.create!(
|
||||
tag_name:,
|
||||
category: :general,
|
||||
deprecated_at: 1.day.from_now
|
||||
)
|
||||
tag_name = create(:tag_name, name: 'normalise deprecated tag')
|
||||
deprecated_tag = create(:tag,
|
||||
primary_tag_name: tag_name,
|
||||
category: :general,
|
||||
deprecated_at: 1.day.from_now)
|
||||
|
||||
expect {
|
||||
described_class.normalise_tags!(
|
||||
[deprecated_tag.name],
|
||||
deny_deprecated: true
|
||||
)
|
||||
described_class.normalise_tags!(Locale.nipponese,
|
||||
[primary_tag_name_for(deprecated_tag, 'ja').name],
|
||||
deny_deprecated: true)
|
||||
}.to raise_error(Tag::DeprecatedTagNormalisationError) { |error|
|
||||
expect(error.tag_names).to eq([deprecated_tag.name])
|
||||
expect(error.tag_names).to eq([primary_tag_name_for(deprecated_tag, 'ja').name])
|
||||
}
|
||||
end
|
||||
|
||||
it 'rejects invalid section literals instead of treating them as zero' do
|
||||
expect {
|
||||
described_class.normalise_tags!(
|
||||
['normalise_invalid_section[1:aa-2:00]'],
|
||||
with_sections: true
|
||||
)
|
||||
described_class.normalise_tags!(Locale.nipponese,
|
||||
['normalise_invalid_section[1:aa-2:00]'],
|
||||
with_sections: true)
|
||||
}.to raise_error(Tag::SectionLiteralParseError)
|
||||
end
|
||||
|
||||
it 'parses open-ended section literals' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[1:00-]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||
end
|
||||
|
||||
it 'parses omitted begin as zero' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[-1:00]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[-1:00]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
|
||||
end
|
||||
|
||||
it 'treats fully open section literals as plain tags' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[-]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[-]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections)[tag.id]).to be_nil
|
||||
end
|
||||
|
||||
it 'treats [0:00-] as a plain tag' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[0:00-]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[0:00-]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections)[tag.id]).to be_nil
|
||||
end
|
||||
|
||||
it 'expands zero-width sections to one millisecond' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-1:00]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[1:00-1:00]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
|
||||
end
|
||||
|
||||
it 'swaps reversed section boundaries' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[2:00-1:00]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[2:00-1:00]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
|
||||
end
|
||||
|
||||
it 'merges open-ended sections over later bounded sections' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-][2:00-3:00]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[1:00-][2:00-3:00]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||
end
|
||||
|
||||
it 'merges adjacent bounded and open-ended sections' do
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-3:00][3:00-]'],
|
||||
with_sections: true
|
||||
)
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
['伊地知ニジカ[1:00-3:00][3:00-]'],
|
||||
with_sections: true)
|
||||
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.expand_parent_tags' do
|
||||
it 'expands through multiple deprecated parents to an active ancestor' do
|
||||
child = create(:tag, name: 'expand_child')
|
||||
deprecated_parent = create(
|
||||
:tag,
|
||||
name: 'expand_deprecated_parent',
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
deprecated_grandparent = create(
|
||||
:tag,
|
||||
name: 'expand_deprecated_grandparent',
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
active_ancestor = create(:tag, name: 'expand_active_ancestor')
|
||||
child = create(:tag, primary_name: 'expand_child')
|
||||
deprecated_parent = create(:tag,
|
||||
primary_name: 'expand_deprecated_parent',
|
||||
deprecated_at: Time.current)
|
||||
deprecated_grandparent = create(:tag,
|
||||
primary_name: 'expand_deprecated_grandparent',
|
||||
deprecated_at: Time.current)
|
||||
active_ancestor = create(:tag, primary_name: 'expand_active_ancestor')
|
||||
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
|
||||
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
|
||||
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
|
||||
@@ -163,8 +196,8 @@ RSpec.describe Tag, type: :model do
|
||||
end
|
||||
|
||||
it 'terminates when implications contain a cycle' do
|
||||
first = create(:tag, name: 'expand_cycle_first')
|
||||
second = create(:tag, name: 'expand_cycle_second')
|
||||
first = create(:tag, primary_name: 'expand_cycle_first')
|
||||
second = create(:tag, primary_name: 'expand_cycle_second')
|
||||
TagImplication.create!(tag: first, parent_tag: second)
|
||||
now = Time.current
|
||||
TagImplication.insert_all!(
|
||||
@@ -183,55 +216,268 @@ RSpec.describe Tag, type: :model do
|
||||
end
|
||||
|
||||
describe '.find_or_create_by_tag_name!' do
|
||||
context 'with an explicit locale' do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'creates the representative name with locale attributes and ownership' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
locale, 'english_name', category: :character)
|
||||
|
||||
expect(primary_tag_name_for(tag, 'en')).to have_attributes(
|
||||
language_code: 'en', script_code: 'Latn',
|
||||
primary_flg: true, tag_id: tag.id)
|
||||
expect(tag.category).to eq('character')
|
||||
end
|
||||
|
||||
it 'uses the English primary as the source for a generated Nipponese name' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
allow(TagName).to receive(:generate_name).and_return('generated_nipponese')
|
||||
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
locale, 'english_origin', category: :general)
|
||||
|
||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
|
||||
.to contain_exactly(have_attributes(
|
||||
name: 'english_origin', auto_generated: false))
|
||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
|
||||
.to contain_exactly(have_attributes(
|
||||
name: 'generated_nipponese', auto_generated: true))
|
||||
source_tag_name = TagName.find_by!(tag:, language_code: 'en', primary_flg: true)
|
||||
expect(TagName).to have_received(:generate_name)
|
||||
.with(Locale.nipponese, tag, source_tag_name)
|
||||
end
|
||||
|
||||
it 'gives a new tag one primary name for every active language' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'new_multilingual_tag', category: :general)
|
||||
|
||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
|
||||
end
|
||||
|
||||
it 'creates one primary per language when multiple locales use English' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
described_class.find_or_create_by_tag_name!(
|
||||
locale, 'before_second_english_locale', category: :general)
|
||||
Locale.create!(code: 'en-x-rspec', language_code: 'en',
|
||||
script_code: 'Latn', region_code: 826,
|
||||
variants: 'x-rspec', name: 'English')
|
||||
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
locale, 'one_english_primary', category: :general)
|
||||
|
||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
|
||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
||||
end
|
||||
|
||||
it 'rolls back a new tag and requested name if name generation fails' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
original_tag_count = described_class.count
|
||||
allow(TagName).to receive(:generate_name).and_raise('generation failed')
|
||||
|
||||
expect {
|
||||
described_class.find_or_create_by_tag_name!(
|
||||
locale, 'atomic_english_name', category: :general)
|
||||
}.to raise_error('generation failed')
|
||||
|
||||
expect(described_class.count).to eq(original_tag_count)
|
||||
expect(TagName.where(language_code: 'en', name: 'atomic_english_name')).to be_empty
|
||||
expect(described_class.joins(:tag_names)
|
||||
.where(tag_names: { name: 'atomic_english_name' })).to be_empty
|
||||
end
|
||||
|
||||
it 'keeps a detached requested name detached if generation fails' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
detached = create(:tag_name, :alias, name: 'atomic_detached',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
original_tag_count = described_class.count
|
||||
allow(TagName).to receive(:generate_name).and_raise('generation failed')
|
||||
|
||||
expect {
|
||||
described_class.find_or_create_by_tag_name!(
|
||||
locale, detached.name, category: :general)
|
||||
}.to raise_error('generation failed')
|
||||
|
||||
expect(described_class.count).to eq(original_tag_count)
|
||||
expect(detached.reload).to have_attributes(tag_id: nil, primary_flg: false)
|
||||
end
|
||||
|
||||
it 'promotes a detached alias into the requested primary name' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
detached = create(:tag_name, :alias, name: 'detached_english',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
locale, detached.name, category: :general)
|
||||
|
||||
expect(detached.reload).to have_attributes(
|
||||
tag_id: tag.id, primary_flg: true, canonical_id: nil,
|
||||
auto_generated: false)
|
||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
|
||||
.to contain_exactly(detached)
|
||||
end
|
||||
|
||||
it 'creates separate tag identities for the same name in different languages' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
nipponese = described_class.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'same_name', category: :general)
|
||||
english = described_class.find_or_create_by_tag_name!(
|
||||
locale, 'same_name', category: :character)
|
||||
|
||||
expect(english).not_to eq(nipponese)
|
||||
expect(primary_tag_name_for(english, 'en')).to have_attributes(
|
||||
name: 'same_name', tag_id: english.id, language_code: 'en', primary_flg: true)
|
||||
expect(primary_tag_name_for(nipponese, 'ja')).to have_attributes(
|
||||
name: 'same_name', tag_id: nipponese.id, language_code: 'ja', primary_flg: true)
|
||||
expect(described_class.find_or_create_by_tag_name!(
|
||||
locale, 'same_name', category: :general)).to eq(english)
|
||||
expect(described_class.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'same_name', category: :general)).to eq(nipponese)
|
||||
end
|
||||
|
||||
it 'preserves the V1 representative when looking up another primary language' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
tag = create(:tag, primary_name: '日本語代表名')
|
||||
representative_id = tag.tag_name_id
|
||||
english = primary_tag_name_for(tag, 'en')
|
||||
english.update!(name: 'english_primary', auto_generated: false)
|
||||
|
||||
found = described_class.find_or_create_by_tag_name!(
|
||||
locale, english.name, category: :general)
|
||||
|
||||
expect(found).to eq(tag)
|
||||
expect(tag.reload.tag_name_id).to eq(representative_id)
|
||||
expect(english.reload.tag_id).to eq(tag.id)
|
||||
end
|
||||
|
||||
it 'normalises names using the supplied locale' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
tags = described_class.normalise_tags!(
|
||||
locale, ['character:normalised_english'],
|
||||
with_tagme: false, with_no_deerjikist: false)
|
||||
|
||||
expect(tags.length).to eq(1)
|
||||
expect(primary_tag_name_for(tags.first, 'en')).to have_attributes(
|
||||
name: 'normalised_english', language_code: 'en', script_code: 'Latn',
|
||||
primary_flg: true, tag_id: tags.first.id)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a tag and name with the requested category after stripping whitespace' do
|
||||
tag = nil
|
||||
|
||||
expect {
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
tag = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
||||
' lookup_new ', category: :character)
|
||||
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
|
||||
|
||||
expect(tag.name).to eq('lookup_new')
|
||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('lookup_new')
|
||||
expect(tag.category).to eq('character')
|
||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
||||
expect(primary_tag_name_for(tag, 'ja').auto_generated).to be(false)
|
||||
end
|
||||
|
||||
it 'reuses the canonical tag for an alias without changing its category' do
|
||||
it 'promotes a detached Nipponese alias into a human-confirmed primary' do
|
||||
detached = create(:tag_name, :alias, name: 'detached_nipponese')
|
||||
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, detached.name, category: :general)
|
||||
|
||||
expect(detached.reload).to have_attributes(
|
||||
tag_id: tag.id, primary_flg: true, canonical_id: nil,
|
||||
auto_generated: false)
|
||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
|
||||
.to contain_exactly(detached)
|
||||
end
|
||||
|
||||
it 'reuses the owning tag for an alias without changing its category' do
|
||||
tag = create(:tag, category: :character)
|
||||
alias_name = TagName.create!(name: 'lookup_alias', canonical: tag.tag_name)
|
||||
representative_id = tag.tag_name_id
|
||||
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: tag.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
found = nil
|
||||
|
||||
expect {
|
||||
found = described_class.find_or_create_by_tag_name!(
|
||||
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
||||
alias_name.name, category: :general)
|
||||
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
|
||||
|
||||
expect(found).to eq(tag)
|
||||
expect(found.category).to eq('character')
|
||||
expect(tag.reload.tag_name_id).to eq(representative_id)
|
||||
end
|
||||
|
||||
it 'creates a tag for an existing canonical name reached through an alias' do
|
||||
canonical = create(:tag_name)
|
||||
alias_name = TagName.create!(name: 'lookup_alias', canonical:)
|
||||
tag = nil
|
||||
it 'reuses the owning tag through another alias' do
|
||||
tag = create(:tag)
|
||||
primary = primary_tag_name_for(tag, 'ja')
|
||||
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
|
||||
other_alias = create(:tag_name, :alias, name: 'lookup_other_alias', tag:)
|
||||
|
||||
expect([alias_name.reload, other_alias.reload]).to all(have_attributes(
|
||||
tag_id: tag.id,
|
||||
primary_flg: false, language_code: 'ja'))
|
||||
|
||||
expect {
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
||||
alias_name.name, category: :general)
|
||||
}.to change(Tag, :count).by(1).and change(TagName, :count).by(0)
|
||||
expect(found).to eq(tag)
|
||||
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
|
||||
|
||||
expect(tag.tag_name).to eq(canonical)
|
||||
expect(primary_tag_name_for(tag.reload, 'ja')).to eq(primary)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.merge_tags!' do
|
||||
let!(:target_tag) { create(:tag, category: :general) }
|
||||
let!(:source_tag) { create(:tag, category: :general) }
|
||||
let!(:source_tag_name) { source_tag.tag_name }
|
||||
let!(:source_tag_name) { primary_tag_name_for(source_tag, 'ja') }
|
||||
|
||||
let!(:post_record) do
|
||||
Post.create!(url: 'https://example.com/posts/1', title: 'test post')
|
||||
end
|
||||
|
||||
context 'with names in multiple languages' do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'keeps the target primary and moves source names as aliases' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
english_primary = primary_tag_name_for(target_tag, 'en')
|
||||
source_primary = primary_tag_name_for(source_tag, 'en')
|
||||
source_alias = create(:tag_name, :alias, tag: source_tag,
|
||||
name: 'source_english_alias', language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect([english_primary, source_primary]).to all(have_attributes(
|
||||
primary_flg: true, auto_generated: true))
|
||||
|
||||
described_class.merge_tags!(target_tag, [source_tag])
|
||||
|
||||
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(english_primary)
|
||||
expect(TagName.where(tag: target_tag, language_code: 'en', primary_flg: true))
|
||||
.to contain_exactly(english_primary)
|
||||
expect([source_primary.reload, source_alias.reload]).to all(have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'en', primary_flg: false))
|
||||
expect(TagName.where(tag: target_tag, language_code: 'ja', primary_flg: true))
|
||||
.to contain_exactly(primary_tag_name_for(target_tag, 'ja'))
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merging a simple source tag' do
|
||||
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
||||
|
||||
@@ -243,7 +489,8 @@ RSpec.describe Tag, type: :model do
|
||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
||||
expect(target_link).to be_present
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -269,15 +516,16 @@ RSpec.describe Tag, type: :model do
|
||||
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
|
||||
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'keeps source history and records the new target alias after deleting the source' do
|
||||
user = create_member_user!
|
||||
source_name = source_tag.name
|
||||
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name)
|
||||
source_name = primary_tag_name_for(source_tag, 'ja').name
|
||||
source_alias = create(:tag_name, :alias, name: 'merge_alias', tag: source_tag)
|
||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
|
||||
original_version = source_tag.tag_versions.first
|
||||
|
||||
@@ -287,13 +535,17 @@ RSpec.describe Tag, type: :model do
|
||||
expect(versions.pluck(:version_no, :event_type))
|
||||
.to eq([[1, 'create'], [2, 'discard']])
|
||||
expect(versions.first).to eq(original_version)
|
||||
expect(versions.last).to have_attributes(
|
||||
name: source_name, aliases: source_alias.name, created_by_user: user)
|
||||
expect(versions.last.created_by_user).to eq(user)
|
||||
expect(versions.last.names.fetch('ja')).to eq(
|
||||
'name' => source_name, 'script_code' => 'Jpan',
|
||||
'auto_generated' => false,
|
||||
'aliases' => [{ 'name' => source_alias.name, 'script_code' => 'Jpan' }])
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
|
||||
target_versions = target_tag.tag_versions.order(:version_no)
|
||||
expect(target_versions.pluck(:event_type)).to eq(['create', 'update'])
|
||||
expect(target_versions.last.aliases.split).to eq([source_name])
|
||||
target_aliases = target_versions.last.names.fetch('ja').fetch('aliases').pluck('name')
|
||||
expect(target_aliases).to contain_exactly('merge_alias', source_name)
|
||||
end
|
||||
|
||||
it 'deletes source relationships while preserving unrelated relationships' do
|
||||
@@ -347,7 +599,8 @@ RSpec.describe Tag, type: :model do
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
||||
expect(target_link).to be_present
|
||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -378,7 +631,8 @@ RSpec.describe Tag, type: :model do
|
||||
expect(target_link).to be_present
|
||||
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -397,7 +651,7 @@ RSpec.describe Tag, type: :model do
|
||||
|
||||
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do
|
||||
earlier_source = create(:tag)
|
||||
earlier_name = earlier_source.tag_name
|
||||
earlier_name = primary_tag_name_for(earlier_source, 'ja')
|
||||
source_section = create(:post_tag_section, post: post_record,
|
||||
tag: source_tag,
|
||||
begin_ms: 1000, end_ms: 2000)
|
||||
@@ -407,7 +661,8 @@ RSpec.describe Tag, type: :model do
|
||||
}.to raise_error(ActiveRecord::RecordInvalid)
|
||||
|
||||
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
|
||||
expect(earlier_name.reload.canonical_id).to be_nil
|
||||
expect(earlier_name.reload).to have_attributes(
|
||||
tag_id: earlier_source.id, language_code: 'ja', primary_flg: true)
|
||||
expect(TagVersion.where(tag_id: [earlier_source.id, source_tag.id, target_tag.id]))
|
||||
.to be_empty
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(true)
|
||||
@@ -416,7 +671,8 @@ RSpec.describe Tag, type: :model do
|
||||
expect(source_post_tag.sections).to contain_exactly(source_section)
|
||||
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
|
||||
expect(source_tag.reload.post_count).to eq(1)
|
||||
expect(source_tag_name.reload.canonical_id).to be_nil
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
|
||||
expect(target_tag.reload.post_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe VersionRecord, type: :model do
|
||||
let!(:tag) { create(:tag, name: 'version_record_tag') }
|
||||
let!(:tag) { create(:tag, primary_name: 'version_record_tag') }
|
||||
let!(:nico_tag) { create(:external_tag, name: 'version_record_tag') }
|
||||
|
||||
it 'makes TagVersion read only after create' do
|
||||
@@ -9,13 +9,14 @@ RSpec.describe VersionRecord, type: :model do
|
||||
tag: tag,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
name: tag.name,
|
||||
name: primary_tag_name_for(tag, 'ja').name,
|
||||
category: tag.category,
|
||||
aliases: '',
|
||||
names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
|
||||
script_code: 'Jpan', auto_generated: false, aliases: [] } },
|
||||
parent_tag_ids: '',
|
||||
created_at: Time.current,
|
||||
created_by_user: nil
|
||||
)
|
||||
created_by_user: nil)
|
||||
|
||||
expect {
|
||||
version.update!(name: 'changed')
|
||||
@@ -27,13 +28,14 @@ RSpec.describe VersionRecord, type: :model do
|
||||
tag: tag,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
name: tag.name,
|
||||
name: primary_tag_name_for(tag, 'ja').name,
|
||||
category: tag.category,
|
||||
aliases: '',
|
||||
names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
|
||||
script_code: 'Jpan', auto_generated: false, aliases: [] } },
|
||||
parent_tag_ids: '',
|
||||
created_at: Time.current,
|
||||
created_by_user: nil
|
||||
)
|
||||
created_by_user: nil)
|
||||
|
||||
expect {
|
||||
version.destroy!
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする