diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 4ca7a17..6b1c633 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -7,6 +7,8 @@ class PostsController < ApplicationController end def index + locale = resolve_locale! + url = params[:url].presence title = params[:title].presence original_created_from = params[:original_created_from].presence @@ -46,7 +48,7 @@ class PostsController < ApplicationController 'COALESCE(pt_max.max_updated_at, posts.updated_at))' q = - filtered_posts + filtered_posts(locale) .joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id") .reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all")) .preload(:external_tags, :uploaded_user, :parents, :children, @@ -102,8 +104,10 @@ class PostsController < ApplicationController end def random + locale = resolve_locale! + post = - filtered_posts + filtered_posts(locale) .preload(:uploaded_user, :parents, :children, post_tags: [:sections, { tag: [:deerjikists, :materials, { tag_name: :wiki_page }] }]) @@ -394,19 +398,19 @@ class PostsController < ApplicationController private - def filtered_posts + def filtered_posts locale tag_names = params[:tags].to_s.split match_type = params[:match] if tag_names.present? - filter_posts_by_tags(tag_names, match_type) + filter_posts_by_tags(locale, tag_names, match_type) else Post.all end end - def filter_posts_by_tags tag_names, match_type + def filter_posts_by_tags locale, tag_names, match_type literals = tag_names.map do |raw_name| - { name: TagName.canonicalise(raw_name.sub(/\Anot:/i, '')).first, + { name: TagName.canonicalise(locale, raw_name.sub(/\Anot:/i, '')).first, negative: raw_name.downcase.start_with?('not:') } end diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index e906737..d8cbcd4 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -430,7 +430,7 @@ class TagsController < ApplicationController else tag.update!(category:, deprecated_at: deprecated ? Time.current : nil) end - rename_tag_name!(tag, name) if name_changed + rename_tag_name!(locale, tag, name) if name_changed alias_names << old_name if name_changed alias_names.delete(name) @@ -701,13 +701,13 @@ class TagsController < ApplicationController false end - def rename_tag_name! tag, name - return if name == tag.name + def rename_tag_name! locale, tag, name + return if name == tag.name(locale.language_code) - current_tag_name = tag.tag_name - target_tag_name = TagName.find_by(name:) + current_tag_name = tag.tag_name(locale.language_code) + target_tag_name = TagName.find_by(language_code: locale.language_code, name:) - if target_tag_name.nil? + unless target_tag_name current_tag_name.update!(name:) return end @@ -722,19 +722,21 @@ class TagsController < ApplicationController TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user) end - promoted_tag_name.update!(canonical: nil) + promoted_tag_name.update!(canonical_id: nil, primary_flg: true) - TagName.where(canonical_id: current_tag_name.id) + TagName.where(tag:, + language_code: current_tag_name.language.language_code, + primary_flg: false) .where.not(id: promoted_tag_name.id) .find_each do |alias_tag_name| - alias_tag_name.update!(canonical: promoted_tag_name) + alias_tag_name.update!(canonical_id: promoted_tag_name, primary_flg: false) end - current_tag_name.wiki_page&.update!(tag_name: promoted_tag_name) - tag.update!(tag_name: promoted_tag_name) + current_tag_name.wiki_page&.update!(tag_name_id: promoted_tag_name.id) + tag.update!(tag_name_id: promoted_tag_name.id) current_tag_name.association(:wiki_page).reset current_tag_name.association(:tag).reset - current_tag_name.reload.update!(canonical: promoted_tag_name) + current_tag_name.reload.update!(canonical_id: promoted_tag_name, primary_flg: false) return unless old_owner_tag && old_owner_tag != tag @@ -766,12 +768,12 @@ class TagsController < ApplicationController current_aliases.each do |alias_tag_name| next if alias_names.include?(alias_tag_name.name) - alias_tag_name.update!(canonical: nil) + alias_tag_name.update!(canonical_id: nil) end alias_names.each do |alias_name| alias_tag_name = TagName.find_or_create_by!(name: alias_name) - alias_tag_name.update!(canonical: tag.tag_name) + alias_tag_name.update!(canonical_id: tag.tag_name.id) end affected_tags.compact.uniq.each do |affected_tag| diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index 2648097..a8cdfd6 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -51,12 +51,10 @@ class Tag < ApplicationRecord has_many :tag_versions has_many :tag_names - belongs_to :tag_name delegate :wiki_page, to: :tag_name attribute :version_no, :integer, default: 1 - delegate :name, to: :tag_name, allow_nil: true validates :tag_name, presence: true enum :category, deerjikist: 'deerjikist', @@ -84,9 +82,8 @@ class Tag < ApplicationRecord 'mtr:' => :material, 'meta:' => :meta }.freeze - def name= val - (self.tag_name ||= build_tag_name).name = val - end + def tag_name(language_code) = tag_names.find_by(language_code:, primary_flg: true) + def name(language_code) = tag_name(language_code)&.name def deprecated? = deprecated_at? @@ -276,7 +273,7 @@ class Tag < ApplicationRecord end TagVersioning.record!(source_tag, event_type: :discard, created_by_user:) - source_tag.tag_names.update_all(tag_id: source_tag.id, primary_key: false, + source_tag.tag_names.update_all(tag_id: target_tag.id, primary_flg: false, updated_at: Time.current) source_tag.destroy! diff --git a/backend/app/models/tag_name.rb b/backend/app/models/tag_name.rb index 91081ee..046cc27 100644 --- a/backend/app/models/tag_name.rb +++ b/backend/app/models/tag_name.rb @@ -13,8 +13,8 @@ class TagName < ApplicationRecord def primary? = primary_flg - def canonical = TagName.find_by(language:, tag:, primary_flg: true) - def aliases = TagName.where(language:, tag:, primary_flg: false) + def canonical = TagName.find_by(language_code:, tag_id:, primary_flg: true) + def aliases = TagName.where(language_code:, tag_id:, primary_flg: false) def self.canonicalise locale, names names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?) @@ -23,7 +23,7 @@ class TagName < ApplicationRecord tns = TagName.where(language_code: locale.language_code, name: names).index_by(&:name) names.map { |name| - if tns[name].primary_flg + if !(tns[name]) || tns[name].primary? name else TagName.find_by(language_code: locale.language_code, diff --git a/backend/app/services/post_create_plan.rb b/backend/app/services/post_create_plan.rb index 6a0f9e6..e1279a1 100644 --- a/backend/app/services/post_create_plan.rb +++ b/backend/app/services/post_create_plan.rb @@ -10,7 +10,7 @@ class PostCreatePlan def build! locale direct_tag_specs, tag_sections = parse_direct_tag_specs(locale) - default_tag_specs = build_default_tag_specs(direct_tag_specs) + default_tag_specs = build_default_tag_specs(locale, direct_tag_specs) snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs) preload_existing_tags_by_name!(snapshot_tag_specs.map { _1[:name] }) validate_new_tag_specs!(snapshot_tag_specs) @@ -263,12 +263,11 @@ class PostCreatePlan end def resolved_tag_name locale, name - tag_name = TagName.find_by!(language_code: locale.language_code, name:) - unless tag_name.primary_flg - tag_name = TagName.find_by!(language_code: locale.language_code, - tag_id: tag_name.tag_id, - primary_flg: true) - end - tag_name.name + tag_name = TagName.find_by(language_code: locale.language_code, name:) + return name if !(tag_name) || tag_name.primary? + + TagName.find_by!(language_code: locale.language_code, + tag_id: tag_name.tag_id, + primary_flg: true).name end end diff --git a/backend/app/services/post_creator.rb b/backend/app/services/post_creator.rb index b200901..f328068 100644 --- a/backend/app/services/post_creator.rb +++ b/backend/app/services/post_creator.rb @@ -74,13 +74,13 @@ class PostCreator normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [], video_ms: @attributes[:video_ms] } else - build_materialised_plan + build_materialised_plan(locale) end end end - def build_materialised_plan - plan = PostCreatePlan.new(attributes: @attributes).build! + def build_materialised_plan locale + plan = PostCreatePlan.new(attributes: @attributes).build!(locale) snapshot_tags = materialise_tags(plan[:snapshot_tag_specs] || []) post_tags = materialise_tags(plan[:post_tag_specs] || []) { diff --git a/backend/spec/factories/tags.rb b/backend/spec/factories/tags.rb index a9df735..b362e23 100644 --- a/backend/spec/factories/tags.rb +++ b/backend/spec/factories/tags.rb @@ -1,20 +1,29 @@ FactoryBot.define do factory :tag do transient do - name { nil } + primary_name { nil } + primary_tag_name do + attributes = primary_name.nil? ? { } : { name: primary_name } + build(:tag_name, **attributes) + end end category { :general } post_count { 0 } - association :tag_name - after(:build) do |tag, evaluator| - tag.name = evaluator.name if evaluator.name.present? + evaluator.primary_tag_name.primary_flg = true + tag.tag_names.target << evaluator.primary_tag_name end - after(:create) do |tag| - tag.tag_name.update!(tag:) if tag.tag_name.tag_id != tag.id + before(:create) do |tag, evaluator| + evaluator.primary_tag_name.save! + # Compatibility write only; ownership is established through tag_id below. + tag.tag_name_id = evaluator.primary_tag_name.id + end + + after(:create) do |tag, evaluator| + evaluator.primary_tag_name.update!(tag:, primary_flg: true) end end end diff --git a/backend/spec/models/locale_spec.rb b/backend/spec/models/locale_spec.rb index a3b328e..54304bf 100644 --- a/backend/spec/models/locale_spec.rb +++ b/backend/spec/models/locale_spec.rb @@ -13,17 +13,19 @@ RSpec.describe Locale, type: :model do end it 'generates a primary name for every existing tag using the name generator' do - first = create(:tag, name: 'first_existing_tag') - second = create(:tag, name: 'second_existing_tag') + 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, first.name) + .with(kind_of(described_class), first, primary_tag_name_for(first, 'ja').name) .and_return('name_from_generator') locale = create_french_locale! - expect(TagName).to have_received(:generate_name).with(locale, first, first.name) - expect(TagName).to have_received(:generate_name).with(locale, second, second.name) + expect(TagName).to have_received(:generate_name) + .with(locale, first, primary_tag_name_for(first, 'ja').name) + expect(TagName).to have_received(:generate_name) + .with(locale, second, primary_tag_name_for(second, 'ja').name) expect(first.tag_names.find_by!(language_code: 'fr', primary_flg: true)) .to have_attributes(name: 'name_from_generator', tag_id: first.id, language_code: 'fr', script_code: 'Latn', @@ -37,7 +39,7 @@ RSpec.describe Locale, type: :model do it 'does not duplicate an existing primary name in the new language' do prepare_french_reference! - tag = create(:tag, name: 'already_named') + tag = create(:tag, primary_name: 'already_named') existing = create(:tag_name, name: 'nom_existant', tag:, language_code: 'fr', script_code: 'Latn') @@ -51,7 +53,7 @@ RSpec.describe Locale, type: :model do it 'creates a primary name when the language has only an alias' do prepare_french_reference! - tag = create(:tag, name: 'alias_only_tag') + tag = create(:tag, primary_name: 'alias_only_tag') alias_name = create(:tag_name, :alias, name: 'alias_fr', tag:, language_code: 'fr', script_code: 'Latn') diff --git a/backend/spec/models/material_export_item_spec.rb b/backend/spec/models/material_export_item_spec.rb index 07e08f3..19734a0 100644 --- a/backend/spec/models/material_export_item_spec.rb +++ b/backend/spec/models/material_export_item_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe MaterialExportItem, type: :model do let(:user) { create(:user, :member) } - let(:tag) { create(:tag, 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) diff --git a/backend/spec/models/post_spec.rb b/backend/spec/models/post_spec.rb index ea5646e..f12d90b 100644 --- a/backend/spec/models/post_spec.rb +++ b/backend/spec/models/post_spec.rb @@ -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 }]) diff --git a/backend/spec/models/post_version_spec.rb b/backend/spec/models/post_version_spec.rb index ccbb20f..f29b530 100644 --- a/backend/spec/models/post_version_spec.rb +++ b/backend/spec/models/post_version_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe PostVersion, type: :model do let!(:tag_name) { create(:tag_name, name: 'post_version_spec_tag') } - let!(:tag) { create(:tag, tag_name: tag_name, category: :general) } + 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| diff --git a/backend/spec/models/tag_implication_spec.rb b/backend/spec/models/tag_implication_spec.rb index a29cc1c..9034d2e 100644 --- a/backend/spec/models/tag_implication_spec.rb +++ b/backend/spec/models/tag_implication_spec.rb @@ -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!( diff --git a/backend/spec/models/tag_name_sanitisation_rule_spec.rb b/backend/spec/models/tag_name_sanitisation_rule_spec.rb index 50d0a10..558869e 100644 --- a/backend/spec/models/tag_name_sanitisation_rule_spec.rb +++ b/backend/spec/models/tag_name_sanitisation_rule_spec.rb @@ -50,17 +50,17 @@ RSpec.describe TagNameSanitisationRule, type: :model do end it 'renames without merging tags or deleting either language identity' do - japanese = create(:tag, name: 'foobar') + japanese = create(:tag, primary_name: 'foobar') english_name = create(:tag_name, name: 'temporary', language_code: 'en', script_code: 'Latn') - english = create(:tag, tag_name: english_name) + 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(japanese.reload.tag_name).to have_attributes( + expect(primary_tag_name_for(japanese.reload, 'ja')).to have_attributes( name: 'foobar', language_code: 'ja', tag_id: japanese.id) expect(english.reload.tag_name_id).to eq(english_name.id) end @@ -96,15 +96,19 @@ RSpec.describe TagNameSanitisationRule, type: :model do context 'when the source tag_name has a tag and the existing one has no tag' do let!(:existing) { create(:tag_name, name: 'foobar') } - let!(:source_tag) { create(:tag, name: 'tmp', category: :general) } - let!(:source_tag_name_id) { source_tag.tag_name_id } + 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! + 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(source_tag.reload.tag_name_id).to eq(existing.id) expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false) end @@ -112,6 +116,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do 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 create(:tag_name, :alias, name: 'foobar', tag: existing_tag) end @@ -129,17 +134,17 @@ RSpec.describe TagNameSanitisationRule, type: :model do 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_tag.tag_name) + .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) { create(:tag_name, name: 'foobar') } - let!(:existing_tag) { create(:tag, tag_name: existing_tn, category: :general) } + let!(:existing_tag) { create(:tag, primary_tag_name: existing_tn, category: :general) } let!(:source_tn) { create(:tag_name, name: 'tmp') } - let!(:source_tag) { create(:tag, tag_name: source_tn, category: :general) } + let!(:source_tag) { create(:tag, primary_tag_name: source_tn, category: :general) } let!(:source_tag_name_id) { source_tn.id } before do @@ -155,7 +160,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 diff --git a/backend/spec/models/tag_name_spec.rb b/backend/spec/models/tag_name_spec.rb index b8e251f..37107cc 100644 --- a/backend/spec/models/tag_name_spec.rb +++ b/backend/spec/models/tag_name_spec.rb @@ -28,16 +28,33 @@ RSpec.describe TagName, type: :model do end it 'persists both sides of the representative tag factory association' do - tag = create(:tag) + name = create(:tag_name) + tag = create(:tag, primary_tag_name: name) - expect(tag.reload.tag_name.reload.tag_id).to eq(tag.id) - expect(tag.tag_name.tag).to eq(tag) + 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(tag.tag_name) + .to eq(name) + expect(tag.reload.tag_name_id).to eq(name.id) + 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 - tag = create(:tag) + 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( @@ -46,12 +63,12 @@ RSpec.describe TagName, type: :model do primary = described_class.find_by!(tag_id: alias_name.tag_id, language_code: alias_name.language_code, primary_flg: true) - expect(primary).to eq(tag.tag_name) + expect(primary).to eq(primary_name) end describe '.canonicalise' do it 'resolves the primary name of the alias language on the same tag' do - tag = create(:tag, name: '日本語正本') + tag = create(:tag, primary_name: '日本語正本') english_primary = create(:tag_name, tag:, name: 'english_primary', language_code: 'en', script_code: 'Latn') english_alias = create(:tag_name, :alias, tag:, name: 'english_alias', @@ -69,10 +86,10 @@ RSpec.describe TagName, type: :model do end it 'resolves only aliases in the requested language' do - japanese = create(:tag, name: 'japanese_canonical') + japanese = create(:tag, primary_name: 'japanese_canonical') english_name = create(:tag_name, name: 'english_canonical', language_code: 'en', script_code: 'Latn') - english = create(:tag, tag_name: english_name) + english = create(:tag, primary_tag_name: english_name) japanese_alias = create(:tag_name, :alias, name: 'shared_alias', tag: japanese) english_alias = create(:tag_name, :alias, name: 'shared_alias', diff --git a/backend/spec/models/tag_spec.rb b/backend/spec/models/tag_spec.rb index 13feb00..d900053 100644 --- a/backend/spec/models/tag_spec.rb +++ b/backend/spec/models/tag_spec.rb @@ -4,7 +4,7 @@ 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 @@ -32,10 +32,10 @@ RSpec.describe Tag, type: :model do { code: 'en', language_code: 'en', script_code: 'Latn', name: 'English', created_at: Time.current }]) unless Locale.exists?(code: 'en') english_locale = Locale.find('en') - japanese = create(:tag, name: 'japanese_canonical') + japanese = create(:tag, primary_name: 'japanese_canonical') english_name = create(:tag_name, name: 'english_canonical', language_code: 'en', script_code: 'Latn') - english = create(:tag, tag_name: english_name) + english = create(:tag, primary_tag_name: english_name) japanese_alias = create(:tag_name, :alias, name: 'shared_alias', tag: japanese) english_alias = create(:tag_name, :alias, name: 'shared_alias', @@ -59,16 +59,16 @@ RSpec.describe Tag, type: :model do it 'rejects deprecated tags when deny_deprecated is enabled' do tag_name = create(:tag_name, name: 'normalise deprecated tag') deprecated_tag = create(:tag, - tag_name:, + primary_tag_name: tag_name, category: :general, deprecated_at: 1.day.from_now) expect { described_class.normalise_tags!(Locale.nipponese, - [deprecated_tag.name], + [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 @@ -85,7 +85,9 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[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 @@ -94,7 +96,9 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[-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 @@ -103,7 +107,9 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[-]'], 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 @@ -112,7 +118,9 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[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 @@ -121,7 +129,9 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[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 @@ -130,7 +140,9 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[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 @@ -139,7 +151,9 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[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 @@ -148,25 +162,23 @@ RSpec.describe Tag, type: :model do ['伊地知ニジカ[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) @@ -183,8 +195,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!( @@ -210,7 +222,7 @@ RSpec.describe Tag, type: :model do tag = described_class.find_or_create_by_tag_name!( locale, 'english_name', category: :character) - expect(tag.tag_name.reload).to have_attributes( + 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') @@ -223,8 +235,10 @@ RSpec.describe Tag, type: :model do locale, 'same_name', category: :character) expect(english).not_to eq(japanese) - expect(english.tag_name.language_code).to eq('en') - expect(japanese.tag_name.language_code).to eq('ja') + 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(japanese, 'ja')).to have_attributes( + name: 'same_name', tag_id: japanese.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!( @@ -232,7 +246,7 @@ RSpec.describe Tag, type: :model do end it 'preserves the V1 representative when looking up another primary language' do - tag = create(:tag, name: '日本語代表名') + tag = create(:tag, primary_name: '日本語代表名') representative_id = tag.tag_name_id english = create(:tag_name, name: 'english_primary', tag:, language_code: 'en', script_code: 'Latn') @@ -251,7 +265,7 @@ RSpec.describe Tag, type: :model do with_tagme: false, with_no_deerjikist: false) expect(tags.length).to eq(1) - expect(tags.first.tag_name).to have_attributes( + 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 @@ -265,7 +279,7 @@ RSpec.describe Tag, type: :model do ' 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') end @@ -290,7 +304,7 @@ RSpec.describe Tag, type: :model do it 'reuses the owning tag through another alias' do tag = create(:tag) - primary = tag.tag_name + 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:) @@ -304,14 +318,14 @@ RSpec.describe Tag, type: :model do expect(found).to eq(tag) }.to change(Tag, :count).by(0).and change(TagName, :count).by(0) - expect(tag.reload.tag_name).to eq(primary) + 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') @@ -363,7 +377,7 @@ RSpec.describe Tag, type: :model do 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_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 @@ -380,7 +394,7 @@ RSpec.describe Tag, type: :model do 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]) + expect(target_versions.last.aliases.split).to contain_exactly('merge_alias', source_name) end it 'deletes source relationships while preserving unrelated relationships' do @@ -486,7 +500,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) diff --git a/backend/spec/models/version_record_spec.rb b/backend/spec/models/version_record_spec.rb index e76f4e7..32a28ed 100644 --- a/backend/spec/models/version_record_spec.rb +++ b/backend/spec/models/version_record_spec.rb @@ -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,7 +9,7 @@ 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: '', parent_tag_ids: '', @@ -27,7 +27,7 @@ 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: '', parent_tag_ids: '', diff --git a/backend/spec/requests/deerjikists_spec.rb b/backend/spec/requests/deerjikists_spec.rb index 136fc2d..8d26658 100644 --- a/backend/spec/requests/deerjikists_spec.rb +++ b/backend/spec/requests/deerjikists_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'Deerjikists API', type: :request do expect(json['tag']).to be_a(Hash) expect(json['tag']['id']).to eq(tag1.id) - expect(json['tag']['name']).to eq(tag1.name) + expect(json['tag']['name']).to eq(primary_tag_name_for(tag1, 'ja').name) end end diff --git a/backend/spec/requests/error_responses_spec.rb b/backend/spec/requests/error_responses_spec.rb index a7b5191..81c3ab9 100644 --- a/backend/spec/requests/error_responses_spec.rb +++ b/backend/spec/requests/error_responses_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'error responses', type: :request do it 'returns a stable field-error payload for unprocessable requests' do member = create(:user, :member) - tag = create(:tag, :general, name: 'error_response_tag') + tag = create(:tag, category: :general, primary_name: 'error_response_tag') sign_in_as(member) patch "/tags/#{ tag.id }", params: { category: 'nico' } diff --git a/backend/spec/requests/gekanator_learning_spec.rb b/backend/spec/requests/gekanator_learning_spec.rb index 855bb75..7154683 100644 --- a/backend/spec/requests/gekanator_learning_spec.rb +++ b/backend/spec/requests/gekanator_learning_spec.rb @@ -898,22 +898,22 @@ RSpec.describe 'Gekanator learning API', type: :request do describe 'GET /gekanator/questions' do it 'omits questions for deprecated tags' do - active_tag = create(:tag, name: 'active_question_tag', category: :general) + active_tag = create(:tag, primary_name: 'active_question_tag', category: :general) deprecated_tag = create(:tag, - name: 'deprecated_question_tag', + primary_name: 'deprecated_question_tag', category: :general, deprecated_at: Time.current) [active_tag, deprecated_tag].each do |question_tag| GekanatorQuestion.create!( - text: "#{ question_tag.name }?", + text: "#{ primary_tag_name_for(question_tag, 'ja').name }?", kind: 'tag', source: 'admin_curated', status: 'accepted', priority_weight: 1.0, condition: { type: 'tag', - key: "#{ question_tag.category }:#{ question_tag.name }" + key: "#{ question_tag.category }:#{ primary_tag_name_for(question_tag, 'ja').name }" }, created_by: admin ) diff --git a/backend/spec/requests/gekanator_posts_spec.rb b/backend/spec/requests/gekanator_posts_spec.rb index 7fe627a..8cfa46f 100644 --- a/backend/spec/requests/gekanator_posts_spec.rb +++ b/backend/spec/requests/gekanator_posts_spec.rb @@ -4,9 +4,9 @@ require 'rails_helper' RSpec.describe 'Gekanator posts API', type: :request do describe 'GET /gekanator/posts' do it 'omits deprecated tags and returns the stored similarity cosine' do - active_tag = create(:tag, name: 'active tag', category: :general) + active_tag = create(:tag, primary_name: 'active tag', category: :general) deprecated_tag = create(:tag, - name: 'deprecated tag', + primary_name: 'deprecated tag', category: :general, deprecated_at: Time.current) post_record = Post.create!(title: 'source', url: 'https://example.com/source') diff --git a/backend/spec/requests/locale_write_paths_spec.rb b/backend/spec/requests/locale_write_paths_spec.rb index 7d269dc..6c0ef8f 100644 --- a/backend/spec/requests/locale_write_paths_spec.rb +++ b/backend/spec/requests/locale_write_paths_spec.rb @@ -96,7 +96,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do tag = create(:tag) put "/tags/#{ tag.id }", params: locale_params.merge( - name: tag.name, category: tag.category, deprecated: false, + name: primary_tag_name_for(tag, 'ja').name, category: tag.category, deprecated: false, aliases: '', parent_tags: 'locale_parent') expect(response).to have_http_status(:ok), response.body @@ -110,11 +110,11 @@ RSpec.describe 'Locale propagation on write paths', type: :request do let!(:english_name) do create(:tag_name, name: 'shared_name', language_code: 'en', script_code: 'Latn') end - let!(:english_tag) { create(:tag, tag_name: english_name, category: :material) } + let!(:english_tag) { create(:tag, primary_tag_name: english_name, category: :material) } let!(:japanese_tag) do - create(:tag, name: 'temporary_japanese', category: :general).tap do |tag| + create(:tag, primary_name: 'temporary_japanese', category: :general).tap do |tag| # Isolate lookup from the separately tested uniqueness validation. - tag.tag_name.update_columns(name: 'shared_name') + primary_tag_name_for(tag, 'ja').update_columns(name: 'shared_name') end end @@ -157,7 +157,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do child = create(:tag) put "/tags/#{ child.id }", params: { - locale: 'en', name: child.name, category: child.category, deprecated: false, + locale: 'en', name: primary_tag_name_for(child, 'ja').name, + category: child.category, deprecated: false, aliases: '', parent_tags: 'shared_name' } expect(response).to have_http_status(:ok), response.body @@ -180,23 +181,24 @@ RSpec.describe 'Locale propagation on write paths', type: :request do let!(:english_name) do create(:tag_name, name: 'english_original', language_code: 'en', script_code: 'Latn') end - let!(:tag) { create(:tag, tag_name: english_name) } + let!(:tag) { create(:tag, primary_tag_name: english_name) } it 'allows a rename to a name already used in Japanese' do - japanese = create(:tag, name: 'rename_target') + japanese = create(:tag, primary_name: 'rename_target') patch "/tags/#{ tag.id }", params: { locale: 'en', name: 'rename_target' } expect(response).to have_http_status(:ok), response.body - expect(tag.reload.tag_name).to have_attributes( + expect(primary_tag_name_for(tag.reload, 'en')).to have_attributes( name: 'rename_target', language_code: 'en', tag_id: tag.id) - expect(japanese.reload.tag_name).to have_attributes( + expect(primary_tag_name_for(japanese.reload, 'ja')).to have_attributes( name: 'rename_target', language_code: 'ja', tag_id: japanese.id) end it 'creates aliases in the requested language' do put "/tags/#{ tag.id }", params: { - locale: 'en', name: tag.name, category: tag.category, deprecated: false, + locale: 'en', name: english_name.name, + category: tag.category, deprecated: false, aliases: 'english_alias', parent_tags: '' } expect(response).to have_http_status(:ok), response.body @@ -210,7 +212,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do end it 'does not take an alias from another language' do - japanese = create(:tag, name: 'japanese_owner') + japanese = create(:tag, primary_name: 'japanese_owner') japanese_alias = create(:tag_name, :alias, name: 'shared_alias', tag: japanese) expect(japanese_alias.reload).to have_attributes( @@ -218,7 +220,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do primary_flg: false, language_code: 'ja') put "/tags/#{ tag.id }", params: { - locale: 'en', name: tag.name, category: tag.category, deprecated: false, + locale: 'en', name: english_name.name, + category: tag.category, deprecated: false, aliases: 'shared_alias', parent_tags: '' } expect(response).to have_http_status(:ok), response.body diff --git a/backend/spec/requests/materials_spec.rb b/backend/spec/requests/materials_spec.rb index 17ff0a9..14b5d8a 100644 --- a/backend/spec/requests/materials_spec.rb +++ b/backend/spec/requests/materials_spec.rb @@ -26,10 +26,10 @@ RSpec.describe 'Materials API', type: :request do describe 'GET /materials' do let!(:tag_a) do - create(:tag, name: 'material_index_a', category: :material) + create(:tag, primary_name: 'material_index_a', category: :material) end let!(:tag_b) do - create(:tag, name: 'material_index_b', category: :material) + create(:tag, primary_name: 'material_index_b', category: :material) end let!(:material_a) do @@ -116,20 +116,20 @@ RSpec.describe 'Materials API', type: :request do it 'filters by descendant tags and returns stable parent tag groups' do root = - create(:tag, name: 'material_scope_root', + create(:tag, primary_name: 'material_scope_root', category: :material) child_b = - create(:tag, name: 'material_scope_b', + create(:tag, primary_name: 'material_scope_b', category: :material) child_a = - create(:tag, name: 'material_scope_a', + create(:tag, primary_name: 'material_scope_a', category: :material) deprecated = - create(:tag, name: 'material_scope_old', + create(:tag, primary_name: 'material_scope_old', category: :material, deprecated_at: Time.current) grandchild = - create(:tag, name: 'material_scope_grandchild', + create(:tag, primary_name: 'material_scope_grandchild', category: :material) root_material = build_material(tag: root, user: member_user, @@ -179,7 +179,7 @@ RSpec.describe 'Materials API', type: :request do describe 'GET /materials/:id' do let!(:tag) do - create(:tag, name: 'material_show', category: :material) + create(:tag, primary_name: 'material_show', category: :material) end let!(:material) do build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png')) @@ -269,7 +269,7 @@ RSpec.describe 'Materials API', type: :request do expect(response).to have_http_status(:created) material = Material.order(:id).last - expect(material.tag.name).to eq('material_create_new') + expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_new') expect(material.tag.category).to eq('material') expect(material.created_by_user).to eq(member_user) expect(material.updated_by_user).to eq(member_user) @@ -297,7 +297,8 @@ RSpec.describe 'Materials API', type: :request do expect(response).to have_http_status(:created) - tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_create_versioned_tag' }) + tag = TagName.find_by!(name: 'material_create_versioned_tag', + language_code: 'ja', primary_flg: true).tag version = tag.tag_versions.order(:version_no).last expect(version.event_type).to eq('create') @@ -324,7 +325,7 @@ RSpec.describe 'Materials API', type: :request do it 'returns 422 when the existing tag is not material/character' do general_tag_name = create(:tag_name, name: 'material_create_general_tag') - create(:tag, tag_name: general_tag_name, category: :general) + create(:tag, primary_tag_name: general_tag_name, category: :general) post '/materials', params: { tag: 'material_create_general_tag', @@ -345,7 +346,7 @@ RSpec.describe 'Materials API', type: :request do expect(response).to have_http_status(:created) material = Material.order(:id).last - expect(material.tag.name).to eq('material_create_url_only') + expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_url_only') expect(material.url).to eq('https://example.com/material-source') expect(material.file.attached?).to be(false) end @@ -384,7 +385,7 @@ RSpec.describe 'Materials API', type: :request do describe 'PUT /materials/:id' do let!(:tag) do - create(:tag, name: 'material_update_old', category: :material) + create(:tag, primary_name: 'material_update_old', category: :material) end let!(:material) do build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png')) @@ -463,7 +464,7 @@ RSpec.describe 'Materials API', type: :request do expect(response).to have_http_status(:ok) material.reload - expect(material.tag.name).to eq('material_update_new') + expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_new') expect(material.tag.category).to eq('material') expect(material.url).to eq('https://example.com/updated-source') expect(material.updated_by_user).to eq(member_user) @@ -497,7 +498,8 @@ RSpec.describe 'Materials API', type: :request do expect(response).to have_http_status(:ok) - tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_update_versioned_tag' }) + tag = TagName.find_by!(name: 'material_update_versioned_tag', + language_code: 'ja', primary_flg: true).tag version = tag.tag_versions.order(:version_no).last expect(version.event_type).to eq('create') @@ -508,7 +510,7 @@ RSpec.describe 'Materials API', type: :request do it 'backfills a create tag_version for an existing material tag without history' do existing_tag = - create(:tag, name: 'material_update_existing_no_history', + create(:tag, primary_name: 'material_update_existing_no_history', category: :material) expect(existing_tag.tag_versions).to be_empty @@ -531,7 +533,7 @@ RSpec.describe 'Materials API', type: :request do it 'backfills a create tag_version for an existing character tag without history' do existing_tag = - create(:tag, name: 'material_update_character_no_history', + create(:tag, primary_name: 'material_update_character_no_history', category: :character) expect(existing_tag.tag_versions).to be_empty @@ -563,7 +565,7 @@ RSpec.describe 'Materials API', type: :request do expect(response).to have_http_status(:ok) material.reload - expect(material.tag.name).to eq('material_update_remove_file') + expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_remove_file') expect(material.url).to eq('https://example.com/updated-source') expect(material.updated_by_user).to eq(member_user) expect(material.file.attached?).to be(false) @@ -651,8 +653,8 @@ RSpec.describe 'Materials API', type: :request do end describe 'GET /materials/download.zip' do - let!(:tag_a) { create(:tag, name: 'zip_a', category: :material) } - let!(:tag_b) { create(:tag, name: 'zip_b', category: :material) } + let!(:tag_a) { create(:tag, primary_name: 'zip_a', category: :material) } + let!(:tag_b) { create(:tag, primary_name: 'zip_b', category: :material) } let!(:material_a) do build_material(tag: tag_a, user: member_user, file: dummy_upload(filename: 'a.png', body: 'zip-a')) @@ -701,7 +703,7 @@ RSpec.describe 'Materials API', type: :request do describe 'GET /materials/versions' do let!(:tag) do - create(:tag, name: 'material_history', category: :material) + create(:tag, primary_name: 'material_history', category: :material) end let!(:material) do build_material(tag:, user: member_user, file: dummy_upload(filename: 'history.png')) @@ -776,7 +778,7 @@ RSpec.describe 'Materials API', type: :request do describe 'DELETE /materials/:id' do let!(:tag) do - create(:tag, name: 'material_destroy', category: :material) + create(:tag, primary_name: 'material_destroy', category: :material) end let!(:material) do build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png')) diff --git a/backend/spec/requests/nico_tags_spec.rb b/backend/spec/requests/nico_tags_spec.rb index f800afb..50fdd0d 100644 --- a/backend/spec/requests/nico_tags_spec.rb +++ b/backend/spec/requests/nico_tags_spec.rb @@ -44,10 +44,10 @@ RSpec.describe 'NicoTags', type: :request do unlinked.update!(name: 'search_unlinked') other = create(:external_tag) other.update!(name: 'other') - destination = create(:tag, :general) - destination.tag_name.update!(name: 'destination_search') + destination = create(:tag, category: :general) + primary_tag_name_for(destination, 'ja').update!(name: 'destination_search') NicoTagRelation.create!(nico_tag: linked, tag: destination) - NicoTagRelation.create!(nico_tag: other, tag: create(:tag, :general)) + NicoTagRelation.create!(nico_tag: other, tag: create(:tag, category: :general)) get '/tags/nico', params: { name: 'search_', @@ -151,7 +151,7 @@ RSpec.describe 'NicoTags', type: :request do it '404 when only an internal tag exists for the target id' do sign_in_as(member) - non_nico = create(:tag, :general) + non_nico = create(:tag, category: :general) expect(ExternalTag.exists?(non_nico.id)).to be(false) patch "/tags/nico/#{non_nico.id}", params: { tags: 'a b' } expect(response).to have_http_status(:not_found) @@ -163,17 +163,17 @@ RSpec.describe 'NicoTags', type: :request do nico_tag = create(:external_tag, name: 'nico_tags_spec_source') linked_a_name = create(:tag_name, name: 'nico_linked_a') - linked_a = create(:tag, tag_name: linked_a_name, category: :general) + linked_a = create(:tag, primary_tag_name: linked_a_name, category: :general) linked_b_name = create(:tag_name, name: 'nico_linked_b') - linked_b = create(:tag, tag_name: linked_b_name, category: :general) + linked_b = create(:tag, primary_tag_name: linked_b_name, category: :general) NicoTagVersionRecorder.record!(external_tag: nico_tag, event_type: :create, created_by_user: admin) expect { patch "/tags/nico/#{nico_tag.id}", params: { - tags: " #{linked_a.name}\n#{linked_b.name} " + tags: " #{ linked_a_name.name }\n#{ linked_b_name.name } " } }.to change(TagVersion, :count).by(2) .and change(NicoTagVersion, :count).by(1) diff --git a/backend/spec/requests/posts_spec.rb b/backend/spec/requests/posts_spec.rb index 580271a..38bc115 100644 --- a/backend/spec/requests/posts_spec.rb +++ b/backend/spec/requests/posts_spec.rb @@ -114,7 +114,7 @@ RSpec.describe 'Posts API', type: :request do end let!(:tag_name) { create(:tag_name, name: 'spec_tag') } - let!(:tag) { create(:tag, tag_name: tag_name, category: :general) } + let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) } let!(:post_record) do Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p| @@ -126,9 +126,9 @@ RSpec.describe 'Posts API', type: :request do let!(:user) { create_member_user! } let!(:tag_name) { create(:tag_name, name: 'spec_tag') } - let!(:tag) { create(:tag, tag_name:, category: :general) } + let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) } let!(:tag_name2) { create(:tag_name, name: 'unko') } - let!(:tag2) { create(:tag, tag_name: tag_name2, category: :deerjikist) } + let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :deerjikist) } let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) } let!(:hit_post) do @@ -267,7 +267,7 @@ RSpec.describe 'Posts API', type: :request do end it 'keeps internal name searches independent of colliding external ids' do - get '/posts', params: { tags: tag.name } + get '/posts', params: { tags: primary_tag_name_for(tag, 'ja').name } expect(response).to have_http_status(:ok) expect(json.fetch('count')).to eq(3) @@ -286,7 +286,7 @@ RSpec.describe 'Posts API', type: :request do [nil, 'all'].each do |match| it "intersects internal and external matches with match=#{ match || 'omitted' }" do - params = { tags: "#{ tag.name } nico:search_external" } + params = { tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external" } params[:match] = match if match get '/posts', params: params @@ -307,7 +307,8 @@ RSpec.describe 'Posts API', type: :request do end it 'excludes external matches from an internal tag search' do - get '/posts', params: { tags: "#{ tag.name } not:nico:search_external" } + get '/posts', params: { + tags: "#{ primary_tag_name_for(tag, 'ja').name } not:nico:search_external" } expect(response).to have_http_status(:ok) expect(json.fetch('count')).to eq(2) @@ -316,7 +317,9 @@ RSpec.describe 'Posts API', type: :request do end it 'unions an external match with a negated internal match' do - get '/posts', params: { tags: "nico:search_external not:#{ tag.name }", match: 'any' } + get '/posts', params: { + tags: "nico:search_external not:#{ primary_tag_name_for(tag, 'ja').name }", + match: 'any' } expect(response).to have_http_status(:ok) expect(json.fetch('count')).to eq(2) @@ -333,7 +336,9 @@ RSpec.describe 'Posts API', type: :request do end it 'applies the same mixed name search to the existing random endpoint' do - get '/posts/random', params: { tags: "#{ tag.name } nico:search_external", match: 'all' } + get '/posts/random', params: { + tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external", + match: 'all' } expect(response).to have_http_status(:ok) expect(json.fetch('id')).to eq(both_post.id) @@ -342,13 +347,13 @@ RSpec.describe 'Posts API', type: :request do context 'when tags contain not:' do let!(:foo_tag_name) { create(:tag_name, name: 'not_spec_foo') } - let!(:foo_tag) { create(:tag, tag_name: foo_tag_name, category: :general) } + let!(:foo_tag) { create(:tag, primary_tag_name: foo_tag_name, category: :general) } let!(:bar_tag_name) { create(:tag_name, name: 'not_spec_bar') } - let!(:bar_tag) { create(:tag, tag_name: bar_tag_name, category: :general) } + let!(:bar_tag) { create(:tag, primary_tag_name: bar_tag_name, category: :general) } let!(:baz_tag_name) { create(:tag_name, name: 'not_spec_baz') } - let!(:baz_tag) { create(:tag, tag_name: baz_tag_name, category: :general) } + let!(:baz_tag) { create(:tag, primary_tag_name: baz_tag_name, category: :general) } let!(:foo_alias_tag_name) do create(:tag_name, :alias, name: 'not_spec_foo_alias', tag: foo_tag) @@ -739,7 +744,9 @@ RSpec.describe 'Posts API', type: :request do .find { _1['name'] == 'nico:post_index_external' } expect(post_json.fetch('tags')).to include( - a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general')) + a_hash_including('id' => tag.id, + 'name' => primary_tag_name_for(tag, 'ja').name, + 'category' => 'general')) expect(external_json).not_to be_nil expect_external_tag_json(external_json, external_tag) end @@ -773,7 +780,7 @@ RSpec.describe 'Posts API', type: :request do it 'omits deprecated tags' do deprecated_tag = create(:tag, - name: 'deprecated_post_tag', + primary_name: 'deprecated_post_tag', category: :general, deprecated_at: Time.current) PostTag.create!(post: post_record, tag: deprecated_tag) @@ -853,7 +860,7 @@ RSpec.describe 'Posts API', type: :request do tags = 15.times.map do |i| tag_name = create(:tag_name, name: "show_query_tag_#{ i }") - tag = create(:tag, tag_name:, category: :general) + tag = create(:tag, primary_tag_name: tag_name, category: :general) create(:tag_name, :alias, name: "show_query_alias_#{ i }", tag:) PostTag.create!(post: post_record, tag:) tag @@ -909,7 +916,9 @@ RSpec.describe 'Posts API', type: :request do expect(response).to have_http_status(:ok) expect(json.fetch('tags')).to include( - a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general')) + a_hash_including('id' => tag.id, + 'name' => primary_tag_name_for(tag, 'ja').name, + 'category' => 'general')) external_json = json .fetch('tags') @@ -1157,7 +1166,7 @@ RSpec.describe 'Posts API', type: :request do it 'rejects a deprecated tag specified directly' do create(:tag, - name: 'deprecated_direct_tag', + primary_name: 'deprecated_direct_tag', category: :general, deprecated_at: Time.current) sign_in_as(member) @@ -1176,16 +1185,16 @@ RSpec.describe 'Posts API', type: :request do end it 'expands through multiple deprecated parent tags and saves active ancestors' do - child = create(:tag, name: 'active_child', category: :general) + child = create(:tag, primary_name: 'active_child', category: :general) deprecated_parent = create(:tag, - name: 'deprecated_parent', + primary_name: 'deprecated_parent', category: :general, deprecated_at: Time.current) deprecated_grandparent = create(:tag, - name: 'deprecated_grandparent', + primary_name: 'deprecated_grandparent', category: :general, deprecated_at: Time.current) - active_grandparent = create(:tag, name: 'active_grandparent', category: :general) + active_grandparent = create(:tag, primary_name: 'active_grandparent', category: :general) 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_grandparent) @@ -1199,7 +1208,8 @@ RSpec.describe 'Posts API', type: :request do ) expect(response).to have_http_status(:created) - saved_names = Post.find(json.fetch('id')).tags.map(&:name) + saved_names = TagName.where(tag_id: Post.find(json.fetch('id')).tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name) expect(saved_names).to include('active_child', 'active_grandparent') expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent') end @@ -1302,7 +1312,8 @@ RSpec.describe 'Posts API', type: :request do expect(response).to have_http_status(:created) created_post = Post.find(json.fetch('id')) - tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' }) + tag = TagName.find_by!(name: '伊地知ニジカ', + language_code: 'ja', primary_flg: true).tag section = PostTagSection.find_by!(post: created_post, tag:) expect(section.begin_ms).to eq(60_000) @@ -1322,7 +1333,8 @@ RSpec.describe 'Posts API', type: :request do expect(response).to have_http_status(:created) created_post = Post.find(json.fetch('id')) - tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' }) + tag = TagName.find_by!(name: '伊地知ニジカ', + language_code: 'ja', primary_flg: true).tag expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil end @@ -1339,7 +1351,8 @@ RSpec.describe 'Posts API', type: :request do expect(response).to have_http_status(:created) created_post = Post.find(json.fetch('id')) - tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' }) + tag = TagName.find_by!(name: '伊地知ニジカ', + language_code: 'ja', primary_flg: true).tag expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil end @@ -1656,7 +1669,7 @@ RSpec.describe 'Posts API', type: :request do begin_ms: 1000, end_ms: 2000) tn2 = create(:tag_name, name: 'spec_tag_2') - replacement_tag = create(:tag, tag_name: tn2, category: :general) + replacement_tag = create(:tag, primary_tag_name: tn2, category: :general) put "/posts/#{post_record.id}", params: post_update_params( post_record, @@ -1705,7 +1718,7 @@ RSpec.describe 'Posts API', type: :request do it 'rejects a deprecated tag specified directly' do create(:tag, - name: 'deprecated_update_tag', + primary_name: 'deprecated_update_tag', category: :general, deprecated_at: Time.current) sign_in_as(member) @@ -1962,7 +1975,7 @@ RSpec.describe 'Posts API', type: :request do put "/posts/#{post_record.id}", params: post_write_params( base_version_no: base_version.version_no, title: 'updated by me', - tags: "spec_tag #{Tag.no_deerjikist.name}") + tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}") expect(response).to have_http_status(:conflict) @@ -1994,7 +2007,8 @@ RSpec.describe 'Posts API', type: :request do put "/posts/#{post_record.id}", params: post_write_params( base_version_no: base_version.version_no, title: post_record.title, - tags: "spec_tag #{Tag.no_deerjikist.name} incoming_added_tag") + tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name, + 'incoming_added_tag'].join(' ')) expect(response).to have_http_status(:conflict) @@ -2024,15 +2038,17 @@ RSpec.describe 'Posts API', type: :request do put "/posts/#{post_record.id}", params: post_write_params( base_version_no: base_version.version_no, title: post_record.title, - tags: "spec_tag #{Tag.no_deerjikist.name} incoming_merge_tag", + tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name, + 'incoming_merge_tag'].join(' '), merge: '1') expect(response).to have_http_status(:ok) - names = post_record.reload.tags.map(&:name) + names = TagName.where(tag_id: post_record.reload.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name) expect(names).to include('spec_tag') - expect(names).to include(Tag.no_deerjikist.name) + expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name) expect(names).to include('current_merge_tag') expect(names).to include('incoming_merge_tag') end @@ -2055,14 +2071,15 @@ RSpec.describe 'Posts API', type: :request do put "/posts/#{post_record.id}", params: post_write_params( base_version_no: base_version.version_no, title: post_record.title, - tags: "spec_tag #{ Tag.no_deerjikist.name }") + tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }") expect(response).to have_http_status(:ok) - names = post_record.reload.tags.map(&:name) + names = TagName.where(tag_id: post_record.reload.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name) expect(names).to include('spec_tag') - expect(names).to include(Tag.no_deerjikist.name) + expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name) expect(post_record.external_tags).to contain_exactly(nico_tag) end @@ -2077,14 +2094,15 @@ RSpec.describe 'Posts API', type: :request do put "/posts/#{post_record.id}", params: post_write_params( base_version_no: base_version.version_no, title: 'updated title', - tags: "spec_tag #{ Tag.no_deerjikist.name }") + tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }") expect(response).to have_http_status(:ok) - names = post_record.reload.tags.map(&:name) + names = TagName.where(tag_id: post_record.reload.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name) expect(names).to include('spec_tag') - expect(names).to include(Tag.no_deerjikist.name) + expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name) expect(post_record.external_tags).to contain_exactly(nico_tag) external_json = @@ -2112,16 +2130,17 @@ RSpec.describe 'Posts API', type: :request do put "/posts/#{post_record.id}", params: post_write_params( base_version_no: base_version.version_no, title: post_record.title, - tags: "spec_tag #{ Tag.no_deerjikist.name }") + tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }") expect(response).to have_http_status(:ok) - names = post_record.reload.tags.map(&:name) + names = TagName.where(tag_id: post_record.reload.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name) expect(post_record.external_tags).to contain_exactly(nico_tag) expect(names).to include('spec_tag') - expect(names).to include(Tag.no_deerjikist.name) - expect(names).not_to include(linked_tag.name) + expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name) + expect(names).not_to include(primary_tag_name_for(linked_tag, 'ja').name) end it 'force-updates stale posts without base_version_no' do @@ -2137,7 +2156,7 @@ RSpec.describe 'Posts API', type: :request do put "/posts/#{post_record.id}", params: post_write_params( title: 'forced title', - tags: "spec_tag #{Tag.no_deerjikist.name}", + tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}", force: '1') expect(response).to have_http_status(:ok) @@ -2191,7 +2210,7 @@ RSpec.describe 'Posts API', type: :request do let(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) } let!(:tag_name2) { create(:tag_name, name: 'spec_tag_2') } - let!(:tag2) { create(:tag, tag_name: tag_name2, category: :general) } + let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :general) } def snapshot_tags(post) post.snapshot_tag_names.join(' ') @@ -2416,7 +2435,7 @@ RSpec.describe 'Posts API', type: :request do expect(response).to have_http_status(:ok) expect(json.fetch('count')).to eq(3) expect(json.fetch('versions').first.fetch('tags')).to include( - 'name' => tag2.name, 'type' => 'context') + 'name' => primary_tag_name_for(tag2, 'ja').name, 'type' => 'context') end it 'filters versions by tag when the current snapshot includes the tag' do @@ -2550,7 +2569,7 @@ RSpec.describe 'Posts API', type: :request do base_version = create_post_version_for!(post_record) tag_name2 = create(:tag_name, name: 'spec_tag_2') - create(:tag, tag_name: tag_name2, category: :general) + create(:tag, primary_tag_name: tag_name2, category: :general) expect do put "/posts/#{post_record.id}", params: post_write_params( @@ -2715,7 +2734,7 @@ RSpec.describe 'Posts API', type: :request do base_version = create_post_version_for!(post_record.reload) tag_name2 = create(:tag_name, name: 'spec_tag_2') - tag2 = create(:tag, tag_name: tag_name2, category: :general) + tag2 = create(:tag, primary_tag_name: tag_name2, category: :general) expect { put "/posts/#{post_record.id}", params: post_write_params( diff --git a/backend/spec/requests/tag_versions_spec.rb b/backend/spec/requests/tag_versions_spec.rb index f9802a4..4aa441b 100644 --- a/backend/spec/requests/tag_versions_spec.rb +++ b/backend/spec/requests/tag_versions_spec.rb @@ -3,13 +3,13 @@ require 'rails_helper' RSpec.describe 'TagVersions API', type: :request do let(:member) { create(:user, :member, name: 'version member') } - let!(:tag) { create(:tag, name: 'tag_versions_target', category: :general) } - let!(:other_tag) { create(:tag, name: 'tag_versions_other', category: :general) } + let!(:tag) { create(:tag, primary_name: 'tag_versions_target', category: :general) } + let!(:other_tag) { create(:tag, primary_name: 'tag_versions_other', category: :general) } - let!(:parent_shared) { create(:tag, name: 'parent_shared', category: :general) } - let!(:parent_old) { create(:tag, name: 'parent_old', category: :general) } - let!(:parent_new) { create(:tag, name: 'parent_new', category: :general) } - let!(:other_parent) { create(:tag, name: 'other_parent', category: :general) } + let!(:parent_shared) { create(:tag, primary_name: 'parent_shared', category: :general) } + let!(:parent_old) { create(:tag, primary_name: 'parent_old', category: :general) } + let!(:parent_new) { create(:tag, primary_name: 'parent_new', category: :general) } + let!(:other_parent) { create(:tag, primary_name: 'other_parent', category: :general) } let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) } let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) } @@ -211,7 +211,7 @@ RSpec.describe 'TagVersions API', type: :request do end it 'returns empty when the specified tag has no versions' do - fresh_tag = create(:tag, name: 'no_versions_tag', category: :general) + fresh_tag = create(:tag, primary_name: 'no_versions_tag', category: :general) get '/tags/versions', params: { id: fresh_tag.id } @@ -233,7 +233,7 @@ RSpec.describe 'TagVersions API', type: :request do it 'does not create tag versions by wiki updates when tag has no versions yet' do wiki_tag_name = create(:tag_name, name: 'tag_versions_from_wiki') - wiki_tag = create(:tag, tag_name: wiki_tag_name, category: :general) + wiki_tag = create(:tag, primary_tag_name: wiki_tag_name, category: :general) wiki_page = Wiki::Commit.create_content!( diff --git a/backend/spec/requests/tag_wiki_history_integrity_spec.rb b/backend/spec/requests/tag_wiki_history_integrity_spec.rb index 33401e9..a464c05 100644 --- a/backend/spec/requests/tag_wiki_history_integrity_spec.rb +++ b/backend/spec/requests/tag_wiki_history_integrity_spec.rb @@ -9,12 +9,12 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do def create_tag! name:, category: :general tag_name = create(:tag_name, name:) - create(:tag, tag_name:, category:) + create(:tag, primary_tag_name: tag_name, category:) end def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body:, created_by_user: user, message: 'init') @@ -43,7 +43,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do wiki_page.reload version = wiki_page.wiki_versions.order(:version_no).last - expect(tag.name).to eq('patch_tag_wiki_after') + expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after') expect(wiki_page.title).to eq('patch_tag_wiki_after') expect(version).to have_attributes( @@ -72,7 +72,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do tag.reload wiki_page.reload - expect(tag.name).to eq('patch_tag_category_only') + expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_category_only') expect(tag.category).to eq('meme') expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions) end @@ -101,7 +101,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do wiki_page.reload version = wiki_page.wiki_versions.order(:version_no).last - expect(tag.name).to eq('put_tag_wiki_after') + expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after') expect(wiki_page.title).to eq('put_tag_wiki_after') expect(version).to have_attributes( @@ -134,7 +134,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do tag.reload wiki_page.reload - expect(tag.name).to eq('put_tag_category_only') + expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_category_only') expect(tag.category).to eq('meme') expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions) end diff --git a/backend/spec/requests/tags_deerjikists_spec.rb b/backend/spec/requests/tags_deerjikists_spec.rb index e0d3a3e..e0c2314 100644 --- a/backend/spec/requests/tags_deerjikists_spec.rb +++ b/backend/spec/requests/tags_deerjikists_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do let(:guest) { create(:user, role: :guest) } before do - tag.tag_name.update!(name: 'deerjika') + primary_tag_name_for(tag, 'ja').update!(name: 'deerjika') end describe 'GET /tags/:id/deerjikists' do @@ -344,7 +344,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do end before do - other_tag.tag_name.update!(name: 'existing-deerjikist') + primary_tag_name_for(other_tag, 'ja').update!(name: 'existing-deerjikist') end it 'returns an indexed 422 error and rolls back the complete replacement' do diff --git a/backend/spec/requests/tags_spec.rb b/backend/spec/requests/tags_spec.rb index 8d10525..07478a0 100644 --- a/backend/spec/requests/tags_spec.rb +++ b/backend/spec/requests/tags_spec.rb @@ -3,12 +3,12 @@ require 'rails_helper' RSpec.describe 'Tags API', type: :request do let!(:tn) { create(:tag_name, name: 'spec_tag') } - let!(:tag) { create(:tag, tag_name: tn, category: :general) } + let!(:tag) { create(:tag, primary_tag_name: tn, category: :general) } let!(:alias_tn) { create(:tag_name, :alias, name: 'unko', tag:) } let!(:post) { Post.create!(url: 'https://example.com/unkounkounko') } let!(:post_tag) { PostTag.create!(post:, tag:) } let!(:tn2) { create(:tag_name, name: 'unknown') } - let!(:tag2) { create(:tag, tag_name: tn2, category: :general) } + let!(:tag2) { create(:tag, primary_tag_name: tn2, category: :general) } def response_tags json.fetch('tags') @@ -38,7 +38,9 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) expect(json.fetch('count')).to eq(2) expect(response_tags).to contain_exactly( - a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'), + a_hash_including('id' => tag.id, + 'name' => primary_tag_name_for(tag, 'ja').name, + 'category' => 'general'), a_hash_including( 'id' => external.id, 'name' => 'nico:spec_external', @@ -91,7 +93,7 @@ RSpec.describe 'Tags API', type: :request do end it 'filters tags by category' do - meme = create(:tag, name: 'meme_only', category: :meme) + meme = create(:tag, primary_name: 'meme_only', category: :meme) get '/tags', params: { category: 'meme' } @@ -102,28 +104,28 @@ RSpec.describe 'Tags API', type: :request do it 'filters tags by deprecated state' do deprecated_tag = create(:tag, - name: 'deprecated_filter', + primary_name: 'deprecated_filter', category: :general, deprecated_at: 1.day.from_now) - active_tag = create(:tag, name: 'active_filter', category: :general) + active_tag = create(:tag, primary_name: 'active_filter', category: :general) get '/tags', params: { name: '_filter', deprecated: '1' } expect(response).to have_http_status(:ok) - expect(response_names).to include(deprecated_tag.name) - expect(response_names).not_to include(active_tag.name) + expect(response_names).to include(primary_tag_name_for(deprecated_tag, 'ja').name) + expect(response_names).not_to include(primary_tag_name_for(active_tag, 'ja').name) get '/tags', params: { name: '_filter', deprecated: '0' } expect(response).to have_http_status(:ok) - expect(response_names).to include(active_tag.name) - expect(response_names).not_to include(deprecated_tag.name) + expect(response_names).to include(primary_tag_name_for(active_tag, 'ja').name) + expect(response_names).not_to include(primary_tag_name_for(deprecated_tag, 'ja').name) end it 'filters tags by post_count range' do - low = create(:tag, name: 'pc_low', category: :general) - mid = create(:tag, name: 'pc_mid', category: :general) - high = create(:tag, name: 'pc_high', category: :general) + low = create(:tag, primary_name: 'pc_low', category: :general) + mid = create(:tag, primary_name: 'pc_mid', category: :general) + high = create(:tag, primary_name: 'pc_high', category: :general) low.update_columns(post_count: 1) mid.update_columns(post_count: 3) @@ -141,8 +143,8 @@ RSpec.describe 'Tags API', type: :request do end it 'filters tags by created_at range' do - old_tag = create(:tag, name: 'created_old', category: :general) - new_tag = create(:tag, name: 'created_new', category: :general) + old_tag = create(:tag, primary_name: 'created_old', category: :general) + new_tag = create(:tag, primary_name: 'created_new', category: :general) old_time = Time.zone.local(2024, 1, 1, 0, 0, 0) new_time = Time.zone.local(2024, 2, 1, 0, 0, 0) @@ -161,8 +163,8 @@ RSpec.describe 'Tags API', type: :request do end it 'filters tags by updated_at range' do - old_tag = create(:tag, name: 'updated_old', category: :general) - new_tag = create(:tag, name: 'updated_new', category: :general) + old_tag = create(:tag, primary_name: 'updated_old', category: :general) + new_tag = create(:tag, primary_name: 'updated_new', category: :general) old_time = Time.zone.local(2024, 3, 1, 0, 0, 0) new_time = Time.zone.local(2024, 4, 1, 0, 0, 0) @@ -181,12 +183,12 @@ RSpec.describe 'Tags API', type: :request do end it 'orders tags by custom category order' do - create(:tag, name: 'cat_deerjikist', category: :deerjikist) - create(:tag, name: 'cat_meme', category: :meme) - create(:tag, name: 'cat_character', category: :character) - create(:tag, name: 'cat_general', category: :general) - create(:tag, name: 'cat_material', category: :material) - create(:tag, name: 'cat_meta', category: :meta) + create(:tag, primary_name: 'cat_deerjikist', category: :deerjikist) + create(:tag, primary_name: 'cat_meme', category: :meme) + create(:tag, primary_name: 'cat_character', category: :character) + create(:tag, primary_name: 'cat_general', category: :general) + create(:tag, primary_name: 'cat_material', category: :material) + create(:tag, primary_name: 'cat_meta', category: :meta) create(:external_tag, name: 'cat_nico') get '/tags', params: { name: 'cat_', order: 'category:asc', limit: 20 } @@ -201,19 +203,19 @@ RSpec.describe 'Tags API', type: :request do context 'with mixed legacy pagination' do let!(:first_tag) do create(:tag, - tag_name: create(:tag_name, name: 'a_mixed_page'), + primary_tag_name: create(:tag_name, name: 'a_mixed_page'), category: :meme) end let!(:middle_tag) do create(:tag, - tag_name: create(:tag_name, name: 'm_mixed_page'), + primary_tag_name: create(:tag_name, name: 'm_mixed_page'), category: :meta) end let!(:last_tag) do create(:tag, - tag_name: create(:tag_name, name: 'z_mixed_page'), + primary_tag_name: create(:tag_name, name: 'z_mixed_page'), category: :general) end @@ -264,7 +266,7 @@ RSpec.describe 'Tags API', type: :request do it 'paginates and keeps total count' do %w[pag_a pag_b pag_c].each do |name| - create(:tag, tag_name: create(:tag_name, name:), category: :general) + create(:tag, primary_tag_name: create(:tag_name, name:), category: :general) end get '/tags', params: { name: 'pag_', order: 'name:asc', page: 2, limit: 2 } @@ -275,8 +277,8 @@ RSpec.describe 'Tags API', type: :request do end it 'falls back to default ordering when order is invalid' do - low = create(:tag, name: 'fallback_low', category: :general) - high = create(:tag, name: 'fallback_high', category: :general) + low = create(:tag, primary_name: 'fallback_low', category: :general) + high = create(:tag, primary_name: 'fallback_high', category: :general) low.update_columns(post_count: 1) high.update_columns(post_count: 9) @@ -289,7 +291,7 @@ RSpec.describe 'Tags API', type: :request do it 'normalises invalid page and limit' do %w[norm_a norm_b].each do |name| - create(:tag, tag_name: create(:tag_name, name:), category: :general) + create(:tag, primary_tag_name: create(:tag_name, name:), category: :general) end get '/tags', params: { name: 'norm_', order: 'name:asc', page: 0, limit: 0 } @@ -302,7 +304,7 @@ RSpec.describe 'Tags API', type: :request do it 'returns aliases and parent tags' do parent_tag = create(:tag, - tag_name: create(:tag_name, name: 'index_parent_tag'), + primary_tag_name: create(:tag_name, name: 'index_parent_tag'), category: :meme) TagImplication.create!(tag:, parent_tag:) @@ -348,7 +350,7 @@ RSpec.describe 'Tags API', type: :request do it 'returns aliases and parent tags' do parent_tag = create(:tag, - tag_name: create(:tag_name, name: 'show_parent_tag'), + primary_tag_name: create(:tag_name, name: 'show_parent_tag'), category: :character) TagImplication.create!(tag:, parent_tag:) @@ -395,7 +397,7 @@ RSpec.describe 'Tags API', type: :request do it 'prefers Tag when Tag and ExternalTag have the same id' do internal = create( :tag, - tag_name: create(:tag_name, name: 'internal_collision')) + primary_tag_name: create(:tag_name, name: 'internal_collision')) create( :external_tag, id: internal.id, @@ -413,7 +415,7 @@ RSpec.describe 'Tags API', type: :request do describe 'GET /tags/autocomplete' do it 'combines internal and external matches without conflating equal ids' do - internal = create(:tag, category: :general, name: 'mixed_internal', post_count: 2) + internal = create(:tag, category: :general, primary_name: 'mixed_internal', post_count: 2) external = create(:external_tag, id: internal.id, name: 'mixed_external', post_count: 3) @@ -452,7 +454,7 @@ RSpec.describe 'Tags API', type: :request do it 'limits the combined results to 20 and sorts ties by displayed name' do 11.times do |i| name = "combined_#{ i.to_s.rjust(2, '0') }" - create(:tag, category: :general, name:, post_count: 1) + create(:tag, category: :general, primary_name: name, post_count: 1) create(:external_tag, name:, post_count: 1) end @@ -464,8 +466,8 @@ RSpec.describe 'Tags API', type: :request do end it 'excludes external tags but preserves internal alias matches when nico is false' do - internal = create(:tag, category: :general, name: 'switch_internal', post_count: 1) - alias_target = create(:tag, category: :general, name: 'alias_target', post_count: 1) + internal = create(:tag, category: :general, primary_name: 'switch_internal', post_count: 1) + alias_target = create(:tag, category: :general, primary_name: 'alias_target', post_count: 1) create(:tag_name, :alias, name: 'switch_alias', tag: alias_target) create(:external_tag, name: 'switch_external', post_count: 1) @@ -473,27 +475,30 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) expect(json).to contain_exactly( - a_hash_including('id' => internal.id, 'name' => internal.name), - a_hash_including('id' => alias_target.id, 'name' => alias_target.name, + a_hash_including('id' => internal.id, + 'name' => primary_tag_name_for(internal, 'ja').name), + a_hash_including('id' => alias_target.id, + 'name' => primary_tag_name_for(alias_target, 'ja').name, 'matched_alias' => 'switch_alias')) end ['%', '_'].each do |wildcard| it "treats #{ wildcard } literally for primary, alias, and external names" do literal = "literal#{ wildcard }match" - create(:tag, category: :general, name: literal, post_count: 1) + create(:tag, category: :general, primary_name: literal, post_count: 1) alias_target = create( - :tag, category: :general, name: 'literal_alias_target', post_count: 1) + :tag, category: :general, primary_name: 'literal_alias_target', post_count: 1) create(:tag_name, :alias, name: "#{ literal }_alias", tag: alias_target) create(:external_tag, name: literal, post_count: 1) - create(:tag, category: :general, name: 'literalXmatch', post_count: 2) + create(:tag, category: :general, primary_name: 'literalXmatch', post_count: 2) create(:external_tag, name: 'literalXmatch', post_count: 2) get '/tags/autocomplete', params: { q: "literal#{ wildcard }" } expect(json.map { |row| row.fetch('name') }) - .to contain_exactly(literal, alias_target.name, "nico:#{ literal }") + .to contain_exactly( + literal, primary_tag_name_for(alias_target, 'ja').name, "nico:#{ literal }") end end @@ -523,7 +528,7 @@ RSpec.describe 'Tags API', type: :request do it 'omits deprecated tags' do deprecated_tag = create(:tag, - name: 'spec_deprecated', + primary_name: 'spec_deprecated', category: :general, deprecated_at: Time.current) deprecated_tag.update_columns(post_count: 1) @@ -619,7 +624,7 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq('new') + expect(primary_tag_name_for(tag, 'ja').name).to eq('new') expect(tag.category).to eq('general') body = JSON.parse(response.body) @@ -634,7 +639,7 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq('spec_tag') + expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag') expect(tag.category).to eq('meme') end @@ -644,7 +649,7 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq('spec_tag') + expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag') expect(tag.category).to eq('general') end @@ -654,7 +659,7 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq('n') + expect(primary_tag_name_for(tag, 'ja').name).to eq('n') expect(tag.category).to eq('meta') end @@ -667,7 +672,7 @@ RSpec.describe 'Tags API', type: :request do patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' } expect(response).to have_http_status(:unprocessable_entity) - expect(tag.reload.name).to eq('spec_tag') + expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag') expect(tag.category).to eq('general') end @@ -717,7 +722,7 @@ RSpec.describe 'Tags API', type: :request do it 'PATCH で tag の name を変更すると対応する wiki version を作成する' do wiki_page = Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body: 'wiki body before', created_by_user: member_user, message: 'init') @@ -745,7 +750,7 @@ RSpec.describe 'Tags API', type: :request do it 'tag の category だけを変更しても wiki version は作成しない' do wiki_page = Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body: 'wiki body before', created_by_user: member_user, message: 'init') @@ -781,8 +786,8 @@ RSpec.describe 'Tags API', type: :request do tag.reload - expect(tag.name).to eq('spec_tag') - expect(tag.tag_name).to have_attributes( + expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag') + expect(primary_tag_name_for(tag, 'ja')).to have_attributes( tag_id: tag.id, language_code: 'ja', primary_flg: true) expect(TagName.find_by!(language_code: 'ja', name: 'patch_roundtrip_target')) .to have_attributes(tag_id: tag.id, primary_flg: false) @@ -791,7 +796,7 @@ RSpec.describe 'Tags API', type: :request do it '別 tag の正規名には変更できない' do wiki_page = Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body: 'patch collision wiki', created_by_user: member_user, message: 'init') @@ -803,10 +808,10 @@ RSpec.describe 'Tags API', type: :request do 'name' => ['その名前は既に使はれてゐます.'] ) - expect(tag.reload.name).to eq('spec_tag') + expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag') expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name)) .to contain_exactly('unko') - expect(wiki_page.reload.tag_name).to eq(tag.tag_name) + expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja')) end it 'system tag の name は変更できない' do @@ -815,34 +820,34 @@ 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(system_tag.reload.name).to eq('bot操作') + expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq('bot操作') end end end describe 'GET /tags/with-depth' do let!(:root_meme) do - create(:tag, name: 'depth_a_root_meme', category: :meme) + create(:tag, primary_name: 'depth_a_root_meme', category: :meme) end let!(:root_material) do - create(:tag, name: 'depth_b_root_material', category: :material) + create(:tag, primary_name: 'depth_b_root_material', category: :material) end let!(:hidden_general_root) do - create(:tag, name: 'depth_hidden_general_root', category: :general) + create(:tag, primary_name: 'depth_hidden_general_root', category: :general) end let!(:child_character) do - create(:tag, name: 'depth_child_character', category: :character) + create(:tag, primary_name: 'depth_child_character', category: :character) end let!(:grandchild_material) do - create(:tag, name: 'depth_grandchild_material', category: :material) + create(:tag, primary_name: 'depth_grandchild_material', category: :material) end let!(:child_general) do - create(:tag, name: 'depth_child_general', category: :general) + create(:tag, primary_name: 'depth_child_general', category: :general) end before do @@ -886,11 +891,11 @@ RSpec.describe 'Tags API', type: :request do it 'passes through deprecated tags when finding children' do deprecated_middle = create(:tag, - name: 'depth_deprecated_middle', + primary_name: 'depth_deprecated_middle', category: :character, deprecated_at: Time.current) visible_descendant = create(:tag, - name: 'depth_visible_descendant', + primary_name: 'depth_visible_descendant', category: :material) TagImplication.create!(parent_tag: root_material, tag: deprecated_middle) TagImplication.create!(parent_tag: deprecated_middle, tag: visible_descendant) @@ -904,18 +909,18 @@ RSpec.describe 'Tags API', type: :request do it 'passes through multiple deprecated tags for roots and has_children' do active_child = create(:tag, - name: 'depth_active_child_below_deprecated', + primary_name: 'depth_active_child_below_deprecated', category: :character) deprecated_parent = create(:tag, - name: 'depth_deprecated_parent', + primary_name: 'depth_deprecated_parent', category: :character, deprecated_at: Time.current) deprecated_grandparent = create(:tag, - name: 'depth_deprecated_grandparent', + primary_name: 'depth_deprecated_grandparent', category: :material, deprecated_at: Time.current) active_ancestor = create(:tag, - name: 'depth_active_ancestor', + primary_name: 'depth_active_ancestor', category: :meme) TagImplication.create!(tag: active_child, parent_tag: deprecated_parent) TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent) @@ -942,10 +947,10 @@ RSpec.describe 'Tags API', type: :request do it 'treats an active tag with only deprecated ancestors as a root' do active_child = create(:tag, - name: 'depth_root_below_deprecated', + primary_name: 'depth_root_below_deprecated', category: :character) deprecated_parent = create(:tag, - name: 'depth_root_deprecated_parent', + primary_name: 'depth_root_deprecated_parent', category: :material, deprecated_at: Time.current) TagImplication.create!(tag: active_child, parent_tag: deprecated_parent) @@ -962,11 +967,11 @@ RSpec.describe 'Tags API', type: :request do it 'terminates when deprecated implications contain a cycle' do first = create(:tag, - name: 'depth_cycle_first', + primary_name: 'depth_cycle_first', category: :character, deprecated_at: Time.current) second = create(:tag, - name: 'depth_cycle_second', + primary_name: 'depth_cycle_second', category: :material, deprecated_at: Time.current) TagImplication.create!(tag: first, parent_tag: root_material) @@ -994,19 +999,19 @@ RSpec.describe 'Tags API', type: :request do let!(:material_user) { create_member_user! } let!(:root_tag) do - create(:tag, name: 'materials_root', category: :material) + create(:tag, primary_name: 'materials_root', category: :material) end let!(:child_a_tag) do - create(:tag, name: 'materials_child_a', category: :material) + create(:tag, primary_name: 'materials_child_a', category: :material) end let!(:child_b_tag) do - create(:tag, name: 'materials_child_b', category: :character) + create(:tag, primary_name: 'materials_child_b', category: :character) end let!(:grandchild_tag) do - create(:tag, name: 'materials_grandchild', category: :material) + create(:tag, primary_name: 'materials_grandchild', category: :material) end let!(:root_material) do @@ -1028,7 +1033,7 @@ RSpec.describe 'Tags API', type: :request do end it 'returns a tag tree with nested materials sorted by child name' do - get "/tags/name/#{ CGI.escape(root_tag.name) }/materials" + get "/tags/name/#{ CGI.escape(primary_tag_name_for(root_tag, 'ja').name) }/materials" expect(response).to have_http_status(:ok) @@ -1119,7 +1124,7 @@ RSpec.describe 'Tags API', type: :request do } expect(response).to have_http_status(:unprocessable_entity) - expect(tag.reload.name).to eq('spec_tag') + expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag') end it 'category が空なら 422 を返す' do @@ -1131,7 +1136,7 @@ RSpec.describe 'Tags API', type: :request do } expect(response).to have_http_status(:unprocessable_entity) - expect(tag.reload.name).to eq('spec_tag') + expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag') expect(tag.category).to eq('general') end @@ -1151,10 +1156,10 @@ RSpec.describe 'Tags API', type: :request do it 'name, category, aliases, parent tags をまとめて更新できる' do old_parent = create(:tag, - tag_name: create(:tag_name, name: 'put_old_parent'), + primary_tag_name: create(:tag_name, name: 'put_old_parent'), category: :general) kept_parent = create(:tag, - tag_name: create(:tag_name, name: 'put_kept_parent'), + primary_tag_name: create(:tag_name, name: 'put_kept_parent'), category: :general) TagImplication.create!(tag:, parent_tag: old_parent) TagImplication.create!(tag:, parent_tag: kept_parent) @@ -1171,7 +1176,7 @@ RSpec.describe 'Tags API', type: :request do tag.reload - expect(tag.name).to eq('put_renamed_tag') + expect(primary_tag_name_for(tag, 'ja').name).to eq('put_renamed_tag') expect(tag.category).to eq('meme') expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_a')) @@ -1179,17 +1184,16 @@ RSpec.describe 'Tags API', type: :request do expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_b')) .to have_attributes(tag_id: tag.id, primary_flg: false) - old_name_alias = TagName.find_by(name: 'spec_tag') + old_name_alias = TagName.find_by(language_code: 'ja', name: 'spec_tag') expect(old_name_alias).to be_present expect(old_name_alias).to have_attributes( tag_id: tag.id, language_code: 'ja', primary_flg: false) expect(alias_tn.reload.tag_id).to be_nil - expect(tag.parents.map(&:name)).to contain_exactly( - 'put_kept_parent', - 'put_new_parent' - ) + expect(TagName.where(tag_id: tag.parents.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name)) + .to contain_exactly('put_kept_parent', 'put_new_parent') expect(TagImplication.where(tag:, parent_tag: old_parent)).not_to exist @@ -1290,7 +1294,7 @@ RSpec.describe 'Tags API', type: :request do tag.reload - expect(tag.name).to eq('spec_tag') + expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag') expect(TagName.find_by!(language_code: 'ja', name: 'put_roundtrip_b')) .to have_attributes(tag_id: tag.id, primary_flg: false) expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name)) @@ -1309,10 +1313,10 @@ RSpec.describe 'Tags API', type: :request do it 'parent tag の snapshot も作成する' do old_parent = create(:tag, - tag_name: create(:tag_name, name: 'put_snapshot_old_parent'), + primary_tag_name: create(:tag_name, name: 'put_snapshot_old_parent'), category: :general) new_parent = create(:tag, - tag_name: create(:tag_name, name: 'put_snapshot_new_parent'), + primary_tag_name: create(:tag_name, name: 'put_snapshot_new_parent'), category: :general) TagImplication.create!(tag:, parent_tag: old_parent) @@ -1320,7 +1324,7 @@ RSpec.describe 'Tags API', type: :request do name: 'spec_tag', category: 'general', aliases: 'unko', - parent_tags: new_parent.name, + parent_tags: primary_tag_name_for(new_parent, 'ja').name, deprecated: '0', } @@ -1343,13 +1347,13 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:unprocessable_entity) - expect(tag.reload.name).to eq('spec_tag') + expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag') expect(tag.category).to eq('general') end it 'system tag の name は変更できない' do system_tag = Tag.tagme - old_name = system_tag.name + old_name = primary_tag_name_for(system_tag, 'ja').name old_category = system_tag.category expect { @@ -1364,14 +1368,14 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:unprocessable_entity) - expect(system_tag.reload.name).to eq(old_name) + expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq(old_name) expect(system_tag.category).to eq(old_category) end it 'wiki を持つ tag を更新すると wiki version も作成する' do wiki_page = Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body: 'wiki body before', created_by_user: member_user, message: 'init') @@ -1410,7 +1414,7 @@ RSpec.describe 'Tags API', type: :request do it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do wiki_page = Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body: 'wiki body before', created_by_user: member_user, message: 'init') @@ -1439,8 +1443,9 @@ RSpec.describe 'Tags API', type: :request do tag.reload - expect(wiki_page.reload.tag_name).to eq(tag.tag_name) - expect(TagName.find_by!(name: 'put_wiki_roundtrip_b').wiki_page).to be_nil + expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja')) + expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b').wiki_page) + .to be_nil expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b')) .to have_attributes(tag_id: tag.id, primary_flg: false) @@ -1452,7 +1457,7 @@ RSpec.describe 'Tags API', type: :request do it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do old_owner = create(:tag, - tag_name: create(:tag_name, name: 'put_alias_old_owner'), + primary_tag_name: create(:tag_name, name: 'put_alias_old_owner'), category: :general) stolen_alias = create(:tag_name, :alias, name: 'put_stolen_alias', @@ -1493,14 +1498,14 @@ RSpec.describe 'Tags API', type: :request do it '別 tag の alias 名を rename で奪へる' do old_owner = create(:tag, - tag_name: create(:tag_name, name: 'put_alias_collision_owner'), + primary_tag_name: create(:tag_name, name: 'put_alias_collision_owner'), category: :general) stolen_alias = create(:tag_name, :alias, name: 'put_alias_collision_name', tag: old_owner) wiki_page = Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body: 'put collision wiki', created_by_user: member_user, message: 'init') @@ -1519,7 +1524,7 @@ RSpec.describe 'Tags API', type: :request do old_owner.reload stolen_alias.reload - expect(tag.name).to eq('put_alias_collision_name') + expect(primary_tag_name_for(tag, 'ja').name).to eq('put_alias_collision_name') expect(stolen_alias).to have_attributes( tag_id: tag.id, language_code: 'ja', primary_flg: true) expect(TagName.find_by!(language_code: 'ja', name: 'spec_tag')) @@ -1532,12 +1537,12 @@ RSpec.describe 'Tags API', type: :request do expect(old_owner_versions.last.event_type).to eq('update') expect(old_owner_versions.last.aliases.split).not_to include('put_alias_collision_name') - expect(wiki_page.reload.tag_name).to eq(tag.tag_name) + expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja')) end it 'parent_tags に指定すると循環する tag は 422 にする' do child = create(:tag, - tag_name: create(:tag_name, name: 'put_cycle_child'), + primary_tag_name: create(:tag_name, name: 'put_cycle_child'), category: :general) TagImplication.create!(tag: child, parent_tag: tag) @@ -1546,7 +1551,7 @@ RSpec.describe 'Tags API', type: :request do name: 'spec_tag', category: 'general', aliases: 'unko', - parent_tags: child.name, + parent_tags: primary_tag_name_for(child, 'ja').name, deprecated: '0', } @@ -1558,7 +1563,7 @@ RSpec.describe 'Tags API', type: :request do it 'tag の name を変更すると対応する wiki version を作成する' do wiki_page = Wiki::Commit.create_content!( - tag_name: tag.tag_name, + tag_name: primary_tag_name_for(tag, 'ja'), body: 'wiki body before', created_by_user: member_user, message: 'init') diff --git a/backend/spec/requests/theatres_spec.rb b/backend/spec/requests/theatres_spec.rb index 7fd3a0a..60fb964 100644 --- a/backend/spec/requests/theatres_spec.rb +++ b/backend/spec/requests/theatres_spec.rb @@ -396,7 +396,7 @@ RSpec.describe 'Theatres API', type: :request do end it 'finalizes skip when votes reach majority and stores voters and tag snapshots' do - tag = create(:tag, name: 'skip-target') + tag = create(:tag, primary_name: 'skip-target') PostTag.create!(post: niconico_post, tag:) TheatreSkipVote.create!(theatre:, post: niconico_post, user: member) @@ -503,7 +503,7 @@ RSpec.describe 'Theatres API', type: :request do end it 'returns tag penalties and candidate weights for the current watchers' do - tag = create(:tag, name: 'heavy-tag') + tag = create(:tag, primary_name: 'heavy-tag') PostTag.create!(post: second_niconico_post, tag:) event = TheatreSkipEvent.create!( theatre:, diff --git a/backend/spec/requests/wiki_spec.rb b/backend/spec/requests/wiki_spec.rb index 7bf7c49..e1d31b1 100644 --- a/backend/spec/requests/wiki_spec.rb +++ b/backend/spec/requests/wiki_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'Wiki API', type: :request do end let!(:tag) do create(:tag, - tag_name: tn, + primary_tag_name: tn, category: :general, deprecated_at: Time.zone.local(2026, 6, 1)) end @@ -275,7 +275,7 @@ RSpec.describe 'Wiki API', type: :request do it 'wiki body だけを変更しても tag version は作成しない' do linked_tag_name = create(:tag_name, name: 'wiki_body_only_tag') - linked_tag = create(:tag, tag_name: linked_tag_name, category: :general) + linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general) TagVersionRecorder.record!( tag: linked_tag, @@ -535,7 +535,7 @@ RSpec.describe 'Wiki API', type: :request do it 'wiki title を変更すると対応する tag の version を作成する' do linked_tag_name = create(:tag_name, name: 'wiki_linked_tag_for_version') - linked_tag = create(:tag, tag_name: linked_tag_name, category: :general) + linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general) linked_page = Wiki::Commit.create_content!( @@ -563,7 +563,8 @@ RSpec.describe 'Wiki API', type: :request do expect(response).to have_http_status(:ok) linked_tag.reload - expect(linked_tag.name).to eq('wiki_linked_tag_for_version_renamed') + expect(primary_tag_name_for(linked_tag, 'ja').name) + .to eq('wiki_linked_tag_for_version_renamed') versions = linked_tag.tag_versions.order(:version_no) diff --git a/backend/spec/services/material_sync_importer_spec.rb b/backend/spec/services/material_sync_importer_spec.rb index 7d34dc6..b2734e3 100644 --- a/backend/spec/services/material_sync_importer_spec.rb +++ b/backend/spec/services/material_sync_importer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe MaterialSyncImporter do let(:user) { create(:user, :member) } - let(:tag) { create(:tag, name: 'sync_tag', category: :material) } + let(:tag) { create(:tag, primary_name: 'sync_tag', category: :material) } def tempfile_for body Tempfile.new(['material-sync-importer', '.png']).tap do |file| diff --git a/backend/spec/services/nico_tag_version_recorder_spec.rb b/backend/spec/services/nico_tag_version_recorder_spec.rb index b7811c4..e48745b 100644 --- a/backend/spec/services/nico_tag_version_recorder_spec.rb +++ b/backend/spec/services/nico_tag_version_recorder_spec.rb @@ -65,7 +65,7 @@ RSpec.describe NicoTagVersionRecorder do end it 'records sorted linked internal names and later link removal' do - tags = ['z_link', 'a_link'].map { |name| create(:tag, name:, category: :general) } + tags = ['z_link', 'a_link'].map { |name| create(:tag, primary_name: name, category: :general) } tags.each { |tag| NicoTagRelation.create!(nico_tag: external_tag, tag:) } expect(record(:create).linked_tags).to eq('a_link z_link') diff --git a/backend/spec/services/post_create_plan_spec.rb b/backend/spec/services/post_create_plan_spec.rb index 209def7..5ff805a 100644 --- a/backend/spec/services/post_create_plan_spec.rb +++ b/backend/spec/services/post_create_plan_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe PostCreatePlan do def create_tag! name, category - create(:tag, name:, category:) + create(:tag, primary_name: name, category:) end before do @@ -16,12 +16,15 @@ RSpec.describe PostCreatePlan do it 'resolves the matching language identity and its parents' do english_name = create(:tag_name, name: 'shared_name', language_code: 'en', script_code: 'Latn') - english = create(:tag, tag_name: english_name, category: :character) - japanese = create(:tag, name: 'temporary_japanese', category: :general) + english = create(:tag, primary_tag_name: english_name, category: :character) + japanese = create(:tag, primary_name: 'temporary_japanese', category: :general) # The DB permits this identity; validation has its own contract spec. - japanese.tag_name.update_columns(name: 'shared_name') - english_parent = create(:tag, name: 'english_parent', category: :material) - japanese_parent = create(:tag, name: 'japanese_parent', category: :general) + primary_tag_name_for(japanese, 'ja').update_columns(name: 'shared_name') + english_parent_name = create(:tag_name, name: 'english_parent', + language_code: 'en', script_code: 'Latn') + english_parent = create(:tag, primary_tag_name: english_parent_name, + category: :material) + japanese_parent = create(:tag, primary_name: 'japanese_parent', category: :general) TagImplication.create!(tag: english, parent_tag: english_parent) TagImplication.create!(tag: japanese, parent_tag: japanese_parent) @@ -30,15 +33,16 @@ RSpec.describe PostCreatePlan do expect(plan[:direct_tag_specs]).to eq( [{ name: 'shared_name', category: :character }]) expect(plan[:post_tag_specs]).to include( - { name: english_parent.name, category: :material }) - expect(plan[:post_tag_specs].pluck(:name)).not_to include(japanese_parent.name) + { name: primary_tag_name_for(english_parent, 'en').name, category: :material }) + expect(plan[:post_tag_specs].pluck(:name)) + .not_to include(primary_tag_name_for(japanese_parent, 'ja').name) end it 'resolves aliases within the requested language' do - japanese = create(:tag, name: 'japanese_canonical') + japanese = create(:tag, primary_name: 'japanese_canonical') english_name = create(:tag_name, name: 'english_canonical', language_code: 'en', script_code: 'Latn') - english = create(:tag, tag_name: english_name, category: :character) + english = create(:tag, primary_tag_name: english_name, category: :character) japanese_alias = create(:tag_name, :alias, name: 'shared_alias', tag: japanese) english_alias = create(:tag_name, :alias, name: 'shared_alias', @@ -55,7 +59,7 @@ RSpec.describe PostCreatePlan do plan = described_class.new(attributes: { tags: 'shared_alias' }).build!(locale) expect(plan[:direct_tag_specs]).to eq( - [{ name: english.name, category: :character }]) + [{ name: english_name.name, category: :character }]) end end diff --git a/backend/spec/services/post_import_previewer_spec.rb b/backend/spec/services/post_import_previewer_spec.rb index 5bc7bcc..cb6888b 100644 --- a/backend/spec/services/post_import_previewer_spec.rb +++ b/backend/spec/services/post_import_previewer_spec.rb @@ -64,7 +64,7 @@ RSpec.describe PostImportPreviewer do end it 'applies metadata to automatic fields and recognises metadata tags' do - create(:tag, name: 'known-tag', category: :general) + create(:tag, primary_name: 'known-tag', category: :general) allow(PostMetadataFetcher).to receive(:fetch).and_return( title: 'metadata title', thumbnail_base: 'https://example.com/thumb.jpg', diff --git a/backend/spec/services/wiki/commit_integrity_spec.rb b/backend/spec/services/wiki/commit_integrity_spec.rb index 9351d75..42c42b7 100644 --- a/backend/spec/services/wiki/commit_integrity_spec.rb +++ b/backend/spec/services/wiki/commit_integrity_spec.rb @@ -77,7 +77,7 @@ RSpec.describe Wiki::Commit do it 'does not record tag_version on body-only wiki update' do tag_name = create(:tag_name, name: 'commit_integrity_linked_tag') - tag = create(:tag, tag_name:, category: :general) + tag = create(:tag, primary_tag_name: tag_name, category: :general) page = described_class.create_content!( diff --git a/backend/spec/services/wiki/commit_spec.rb b/backend/spec/services/wiki/commit_spec.rb index 5d82b34..122ac58 100644 --- a/backend/spec/services/wiki/commit_spec.rb +++ b/backend/spec/services/wiki/commit_spec.rb @@ -81,7 +81,7 @@ RSpec.describe Wiki::Commit do it 'does not record tag version when corresponding tag has no versions' do tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions') - tag = create(:tag, tag_name:, category: :general) + tag = create(:tag, primary_tag_name: tag_name, category: :general) page = described_class.create_content!( @@ -108,7 +108,7 @@ RSpec.describe Wiki::Commit do it 'does not record tag version when corresponding tag has no versions' do tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions') - tag = create(:tag, tag_name:, category: :general) + tag = create(:tag, primary_tag_name: tag_name, category: :general) page = described_class.create_content!( diff --git a/backend/spec/support/test_records.rb b/backend/spec/support/test_records.rb index b6a7cd5..a814e68 100644 --- a/backend/spec/support/test_records.rb +++ b/backend/spec/support/test_records.rb @@ -1,4 +1,8 @@ module TestRecords + def primary_tag_name_for(tag, language_code) + TagName.find_by!(tag_id: tag.id, language_code:, primary_flg: true) + end + def create_member_user! User.create!(name: 'spec user', inheritance_code: SecureRandom.hex(16), diff --git a/backend/spec/tasks/nico_sync_spec.rb b/backend/spec/tasks/nico_sync_spec.rb index e62a8f3..6ae8afa 100644 --- a/backend/spec/tasks/nico_sync_spec.rb +++ b/backend/spec/tasks/nico_sync_spec.rb @@ -64,7 +64,8 @@ RSpec.describe 'nico:sync' do run_rake_task('nico:sync') post.reload - active_tag_names = post.tags.joins(:tag_name).pluck('tag_names.name') + active_tag_names = TagName.where(tag_id: post.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name) expect(active_tag_names).to include('spec_kept') expect(post.external_tags).to contain_exactly(nico) @@ -434,7 +435,9 @@ RSpec.describe 'nico:sync' do external = post.external_tags.sole expect(external.name).to eq('raw tag[]') - expect(post.tags.map(&:name)).not_to include('bot操作') + expect(TagName.where(tag_id: post.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name)) + .not_to include('bot操作') expect(post.post_versions.order(:version_no).last.tags_json) .to include('external_tag_id' => external.id) @@ -446,7 +449,9 @@ RSpec.describe 'nico:sync' do run_nico_sync_with_tags!([]) }.to change(PostVersion, :count).by(1) expect(post.reload.external_tags).to be_empty - expect(post.tags.map(&:name)).not_to include('bot操作') + expect(TagName.where(tag_id: post.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name)) + .not_to include('bot操作') end it 'サニタイズ後の既存外部タグを再利用し、その連携タグを記載する' do @@ -563,7 +568,9 @@ RSpec.describe 'nico:sync' do name: 'AAA') expect(ExternalTag.exists?(platform: :nico, name: 'AAA?')).to be(false) - expect(post.tags.map(&:name)).not_to include('bot操作') + expect(TagName.where(tag_id: post.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name)) + .not_to include('bot操作') expect(post.post_versions.order(:version_no).last.tags_json) .to include('external_tag_id' => external.id) @@ -577,7 +584,9 @@ RSpec.describe 'nico:sync' do }.to change(PostVersion, :count).by(1) expect(post.reload.external_tags).to be_empty - expect(post.tags.map(&:name)).not_to include('bot操作') + expect(TagName.where(tag_id: post.tags.select(:id), + language_code: 'ja', primary_flg: true).pluck(:name)) + .not_to include('bot操作') end it 'nico: prefix を含む従来の規則で外部タグ名をサニタイズする' do diff --git a/backend/spec/tasks/post_similarity_calc_spec.rb b/backend/spec/tasks/post_similarity_calc_spec.rb index 1934f66..721be6c 100644 --- a/backend/spec/tasks/post_similarity_calc_spec.rb +++ b/backend/spec/tasks/post_similarity_calc_spec.rb @@ -6,10 +6,10 @@ RSpec.describe 'post_similarity:calc' do it 'calculates similarities from active tags only' do # 必要最低限のデータ - t1 = create(:tag, name: 't1') - t2 = create(:tag, name: 't2') - t3 = create(:tag, name: 't3') - deprecated_tag = create(:tag, name: 'deprecated', deprecated_at: Time.current) + t1 = create(:tag, primary_name: 't1') + t2 = create(:tag, primary_name: 't2') + t3 = create(:tag, primary_name: 't3') + deprecated_tag = create(:tag, primary_name: 'deprecated', deprecated_at: Time.current) p1 = Post.create!(url: "https://example.com/1") p2 = Post.create!(url: "https://example.com/2") diff --git a/backend/spec/tasks/tag_similarity_calc_spec.rb b/backend/spec/tasks/tag_similarity_calc_spec.rb index fe293a6..a5aae72 100644 --- a/backend/spec/tasks/tag_similarity_calc_spec.rb +++ b/backend/spec/tasks/tag_similarity_calc_spec.rb @@ -6,10 +6,10 @@ RSpec.describe 'tag_similarity:calc' do it 'calculates similarities for active tags only' do # 必要最低限のデータ - t1 = create(:tag, name: 't1') - t2 = create(:tag, name: 't2') - t3 = create(:tag, name: 't3') - deprecated_tag = create(:tag, name: 'deprecated', deprecated_at: Time.current) + t1 = create(:tag, primary_name: 't1') + t2 = create(:tag, primary_name: 't2') + t3 = create(:tag, primary_name: 't3') + deprecated_tag = create(:tag, primary_name: 'deprecated', deprecated_at: Time.current) p1 = Post.create!(url: "https://example.com/1") p2 = Post.create!(url: "https://example.com/2")