このコミットが含まれているのは:
2026-09-27 03:47:27 +09:00
コミット 701830b643
18個のファイルの変更、260行の追加、187行の削除
+3 -1
ファイルの表示
@@ -133,12 +133,14 @@ class MaterialsController < ApplicationController
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
return render_material_import_block(block) if block
locale = resolve_locale!
uploaded_blob = build_uploaded_material_blob!(file, file_sha256)
begin
Material.transaction do
MaterialVersionRecorder.ensure_snapshot!(material, created_by_user: current_user)
tag = resolve_material_tag!(tag_name_raw)
tag = resolve_material_tag!(locale, tag_name_raw)
material.assign_attributes(tag:, url:, updated_by_user: current_user)
if uploaded_blob
material.file.attach(uploaded_blob)
+7 -2
ファイルの表示
@@ -218,7 +218,7 @@ class PostsController < ApplicationController
preflight = PostCreatePreflight.new(
attributes: post_create_attributes,
thumbnail: params[:thumbnail],
host: request.base_url).run
host: request.base_url).run(locale)
return render json: dry_run_json(preflight) if bool?(:dry)
if preflight[:existing_post_id].present?
post = Post.new(url: preflight[:url])
@@ -273,13 +273,18 @@ class PostsController < ApplicationController
return head :forbidden unless current_user.gte_member?
return head :unsupported_media_type unless request.content_mime_type == Mime[:multipart_form]
return head :payload_too_large if request.content_length.to_i > MAX_BULK_REQUEST_BYTES
locale = resolve_locale!
posts = parse_bulk_posts_manifest
thumbnails = parse_bulk_thumbnails(posts.length)
result = PostBulkCreator.new(
actor: current_user,
posts:,
thumbnails:,
host: request.base_url).run
host: request.base_url).run(locale)
render json: result
rescue JSON::ParserError
render_bad_request 'posts manifest の JSON が不正です.'
+8 -7
ファイルの表示
@@ -406,10 +406,10 @@ class TagsController < ApplicationController
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
return render_unprocessable_entity '廃止状態は必須です.', field: :deprecated unless params.key?(:deprecated)
return unless validate_tag_rename(tag, name)
locale = resolve_locale!
return unless validate_tag_rename(locale, tag, name)
alias_names = params[:aliases].to_s.split.uniq
parent_names = params[:parent_tags].to_s.split.uniq
deprecated = bool?(:deprecated)
@@ -455,6 +455,8 @@ class TagsController < ApplicationController
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
name = params[:name].presence
category = params[:category].presence
deprecated_given = params.key?(:deprecated)
@@ -462,7 +464,7 @@ class TagsController < ApplicationController
tag = Tag.find(params[:id])
return unless validate_tag_rename(tag, name)
return unless validate_tag_rename(locale, tag, name)
if category.present? && category == 'nico'
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
@@ -684,7 +686,7 @@ class TagsController < ApplicationController
created_by_user:)
end
def validate_tag_rename tag, name
def validate_tag_rename locale, tag, name
return true if name.blank? || name == tag.name
if tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico])
@@ -692,9 +694,8 @@ class TagsController < ApplicationController
return false
end
target_tag_name = TagName.find_by(name:)
return true if target_tag_name.nil?
return true if target_tag_name.canonical_id?
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
return true unless target_tag_name&.primary_flg
render_unprocessable_entity 'その名前は既に使はれてゐます.', field: :name
false
+4 -5
ファイルの表示
@@ -135,7 +135,7 @@ class Tag < ApplicationRecord
raise SectionLiteralParseError.new(raw_name, raw_name)
end
name = TagName.canonicalise(name).first
name = TagName.canonicalise(locale, name).first
find_or_create_by_tag_name!(locale, name, category: (cat || :general)).tap do |tag|
if deny_deprecated && tag.deprecated?
@@ -235,10 +235,7 @@ class Tag < ApplicationRecord
end
tag = tn.tag
if tag
tag.update!(tag_name_id: tn.id) if tag.tag_name_id != tn.id
return tag
end
return tag if tag
tag = Tag.create!(tag_name: tn, category:)
tn.update!(tag:)
@@ -279,6 +276,8 @@ 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,
updated_at: Time.current)
source_tag.destroy!
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
+25 -19
ファイルの表示
@@ -1,24 +1,36 @@
class TagName < ApplicationRecord
belongs_to :language, foreign_key: :language_code, primary_key: :code
belongs_to :tag, optional: true
has_one :wiki_page
belongs_to :canonical, class_name: 'TagName', optional: true
has_many :aliases, class_name: 'TagName', foreign_key: :canonical_id
validates :name, presence: true,
length: { maximum: 255 },
uniqueness: { scope: :language_code }
validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
validate :canonical_must_be_canonical
validate :alias_name_must_not_have_prefix
validate :canonical_must_not_be_present_with_tag_or_wiki_page
validate :alias_must_not_have_wiki_page
validate :name_must_be_sanitised
def self.canonicalise names
def primary? = primary_flg
def canonical = TagName.find_by(language:, tag:, primary_flg: true)
def aliases = TagName.where(language:, tag:, primary_flg: false)
def self.canonicalise locale, names
names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?)
return [] if names.blank?
tns = TagName.includes(:canonical).where(name: names).index_by(&:name)
tns = TagName.where(language_code: locale.language_code, name: names).index_by(&:name)
names.map { |name| tns[name]&.canonical&.name || name }.uniq
names.map { |name|
if tns[name].primary_flg
name
else
TagName.find_by(language_code: locale.language_code,
tag_id: tns[name].tag_id,
primary_flg: true).name
end
}.uniq
end
def self.generate_name locale, tag, name
@@ -29,21 +41,15 @@ class TagName < ApplicationRecord
private
def canonical_must_be_canonical
if canonical&.canonical_id?
errors.add :canonical, 'canonical は実体を示す必要があります.'
end
end
def alias_name_must_not_have_prefix
if canonical_id? && name.to_s.include?(':')
if !(primary?) && name.to_s.include?(':')
errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.'
end
end
def canonical_must_not_be_present_with_tag_or_wiki_page
if canonical_id? && (tag || wiki_page)
errors.add :canonical, 'タグもしくは Wiki の参照がある名前はエーリアスになれません.'
def alias_must_not_have_wiki_page
if !(primary?) && wiki_page
errors.add :primary_flg, 'Wiki 参照がある名前はエーリアスになれません.'
end
end
+26 -19
ファイルの表示
@@ -15,31 +15,38 @@ class TagNameSanitisationRule < ApplicationRecord
end
def apply!
TagName.find_each do |tn|
name = sanitise(tn.name)
next if name == tn.name
Language.find_each do |language|
TagName.where(language:).find_each do |tn|
name = sanitise(tn.name)
next if name == tn.name
TagName.transaction do
existing_tn = TagName.find_by(name:)
if existing_tn
existing_tn = existing_tn.canonical || existing_tn
next if existing_tn.id == tn.id
TagName.transaction do
existing_tn = TagName.find_by(language:, name:)
if existing_tn
unless existing_tn.primary_flg
existing_tn = TagName.find_by!(language:,
tag_id: existing_tn.tag_id,
primary_flg: true)
end
next if existing_tn.id == tn.id
existing_tag = Tag.find_by(tag_name_id: existing_tn.id)
source_tag = Tag.find_by(tag_name_id: tn.id)
existing_tag = existing_tn.tag
source_tag = tn.tag
if existing_tag
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
elsif source_tag
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
if existing_tag
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
elsif source_tag
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
existing_tn.update_columns(tag_id: source_tag.id, updated_at: Time.current)
end
tn.destroy!
next
end
tn.destroy!
next
# TagName 側の自動サニタイズを回避
tn.update_columns(name:, updated_at: Time.current)
end
# TagName 側の自動サニタイズを回避
tn.update_columns(name:, updated_at: Time.current)
end
end
end
+32 -27
ファイルの表示
@@ -8,13 +8,13 @@ class PostCreatePlan
@existing_tags_by_name = nil
end
def build!
direct_tag_specs, tag_sections = parse_direct_tag_specs
def build! locale
direct_tag_specs, tag_sections = parse_direct_tag_specs(locale)
default_tag_specs = build_default_tag_specs(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)
post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
post_tag_specs = expand_parent_tag_specs(locale, snapshot_tag_specs)
video_ms = normalise_video_ms(snapshot_tag_specs)
validate_video_sections!(video_ms, tag_sections)
parent_post_ids = normalise_parent_post_ids
@@ -42,13 +42,13 @@ class PostCreatePlan
def tag_names = @attributes[:tags].to_s.split
def parse_direct_tag_specs
def parse_direct_tag_specs locale
tag_sections = { }
direct_tag_specs = []
tag_names.each do |raw_name|
tag_name, category, sections = parse_raw_tag_name(raw_name)
existing_tag = existing_tags_by_name[tag_name]
tag_name, category, sections = parse_raw_tag_name(locale, raw_name)
existing_tag = existing_tags_by_name(locale)[tag_name]
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
direct_tag_specs << {
@@ -65,7 +65,7 @@ class PostCreatePlan
[merge_tag_specs(direct_tag_specs), tag_sections]
end
def parse_raw_tag_name raw_name
def parse_raw_tag_name locale, raw_name
name = raw_name.to_s
prefix, category =
Tag::CATEGORY_PREFIXES.find {
@@ -74,7 +74,7 @@ class PostCreatePlan
name = name.sub(/\A#{ prefix }/i, '')
sections = []
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
while match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/)
name = "#{ match[1] }#{ match[4] }"
next if match[2].empty? && match[3].empty?
@@ -87,17 +87,17 @@ class PostCreatePlan
raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
end
[resolved_tag_name(name), category&.to_sym, sections]
[resolved_tag_name(locale, name), category&.to_sym, sections]
end
def build_default_tag_specs direct_tag_specs
def build_default_tag_specs locale, direct_tag_specs
default_tag_specs = []
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
default_tag_specs << {
name: TAGME_TAG_NAME,
category: :meta }
end
if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
default_tag_specs << {
name: NO_DEERJIKIST_TAG_NAME,
category: :meta }
@@ -131,8 +131,10 @@ class PostCreatePlan
raise ActiveRecord::RecordInvalid, post
end
def expand_parent_tag_specs snapshot_tag_specs
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
def expand_parent_tag_specs locale, snapshot_tag_specs
existing_snapshot_tags = snapshot_tag_specs.filter_map do
existing_tags_by_name(locale)[_1[:name]]
end
expanded_parent_specs =
Tag.expand_parent_tags(existing_snapshot_tags)
.reject(&:deprecated?)
@@ -156,33 +158,33 @@ class PostCreatePlan
}.values.sort_by { _1[:name] }
end
def existing_tags_by_name
def existing_tags_by_name locale
@existing_tags_by_name ||= begin
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
end
end
def preload_existing_tags_by_name! names
def preload_existing_tags_by_name! locale, names
wanted_names = Array(names).map { _1.to_s }.reject(&:blank?).uniq
missing_names = wanted_names - existing_tags_by_name.keys
missing_names = wanted_names - existing_tags_by_name(locale).keys
return if missing_names.empty?
existing_tags_by_name.merge!(
existing_tags_by_name(locale).merge!(
Tag.joins(:tag_name)
.where(tag_names: { name: missing_names })
.index_by(&:name))
end
def canonical_tag_name_without_sections raw_name
name, = parse_raw_tag_name(raw_name)
def canonical_tag_name_without_sections locale, raw_name
name, = parse_raw_tag_name(locale, raw_name)
name
end
def deerjikist_tag_spec? spec
def deerjikist_tag_spec? locale, spec
return true if spec[:category] == :deerjikist
existing_tags_by_name[spec[:name]]&.deerjikist?
existing_tags_by_name(locale)[spec[:name]]&.deerjikist?
end
def normalise_parent_post_ids
@@ -260,10 +262,13 @@ class PostCreatePlan
end
end
def resolved_tag_name name
tag_name = TagName.includes(:canonical).find_by(name:)
return name if tag_name.nil?
(tag_name.canonical || tag_name).name
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
end
end
+10 -8
ファイルの表示
@@ -21,14 +21,14 @@ class PostCreator
ApplicationRecord.transaction do
post.save!
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
snapshot_tags = planned_snapshot_tags
post_tags = planned_post_tags
sections = planned_sections
snapshot_tags = planned_snapshot_tags(locale)
post_tags = planned_post_tags(locale)
sections = planned_sections(locale)
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
post.video_ms = planned_video_ms(locale)
post.save!
sync_post_tags!(post, post_tags, sections)
sync_parent_posts!(post, planned_parent_post_ids)
sync_parent_posts!(post, planned_parent_post_ids(locale))
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
end
post
@@ -45,13 +45,15 @@ class PostCreator
thumbnail_base: @attributes[:thumbnail_base].presence)
end
def planned_snapshot_tags = planned_create_attributes[:snapshot_tags]
def planned_snapshot_tags(locale) = planned_create_attributes(locale)[:snapshot_tags]
def planned_post_tags = planned_create_attributes[:post_tags]
def planned_post_tags(locale) = planned_create_attributes(locale)[:post_tags]
def planned_sections = planned_create_attributes[:tag_sections]
def planned_sections(locale) = planned_create_attributes(locale)[:tag_sections]
def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]
def planned_parent_post_ids locale
planned_create_attributes(locale)[:normalised_parent_post_ids]
end
def planned_video_ms locale
planned_create_attributes(locale)[:video_ms]
+1
ファイルの表示
@@ -11,6 +11,7 @@ RSpec.describe 'discarded tag cleanup migrations' do
end
context 'with legacy records' do
# Reproduce the historical schema required by these pre-multilingual migrations.
self.use_transactional_tests = false
before do
-9
ファイルの表示
@@ -7,15 +7,6 @@ FactoryBot.define do
trait :alias do
primary_flg { false }
after(:create) do |tag_name|
owner = tag_name.canonical&.tag
raise ArgumentError, 'alias requires a canonical tag' unless owner
# The alias validation still rejects tag_id; its contract is tested separately.
tag_name.update_columns(tag_id: owner.id)
tag_name.association(:tag).reset
end
end
end
end
+4 -4
ファイルの表示
@@ -27,10 +27,10 @@ RSpec.describe Locale, type: :model do
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',
primary_flg: true, canonical_id: nil)
primary_flg: true)
expect(second.tag_names.find_by!(language_code: 'fr', primary_flg: true))
.to have_attributes(tag_id: second.id, language_code: 'fr',
script_code: 'Latn', primary_flg: true, canonical_id: nil)
script_code: 'Latn', primary_flg: true)
expect(TagName.where(language_code: 'fr', primary_flg: true,
tag_id: [first.id, second.id]).count).to eq(2)
end
@@ -53,13 +53,13 @@ RSpec.describe Locale, type: :model do
prepare_french_reference!
tag = create(:tag, name: 'alias_only_tag')
alias_name = create(:tag_name, :alias, name: 'alias_fr',
canonical: tag.tag_name,
tag:,
language_code: 'fr', script_code: 'Latn')
create_french_locale!
expect(alias_name.reload).to have_attributes(
canonical_id: tag.tag_name_id, tag_id: tag.id,
tag_id: tag.id,
language_code: 'fr', script_code: 'Latn', primary_flg: false)
generated = TagName.find_by!(tag_id: tag.id, language_code: 'fr', primary_flg: true)
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'fr',
+9 -6
ファイルの表示
@@ -79,7 +79,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end
end
context 'when a conflicting canonical tag_name exists' do
context 'when a conflicting primary tag name exists' do
let!(:existing) { create(:tag_name, name: 'foobar') }
let!(:source) do
create(:tag_name, name: 'tmp').tap do |tn|
@@ -105,8 +105,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
it 'moves the tag to the existing tag_name' do
described_class.apply!
expected_tag_name_id = existing.canonical_id || existing.id
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
expect(source_tag.reload.tag_name_id).to eq(existing.id)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
end
end
@@ -114,7 +113,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!(:alias_name) do
create(:tag_name, :alias, name: 'foobar', canonical: existing_tag.tag_name)
create(:tag_name, :alias, name: 'foobar', tag: existing_tag)
end
let!(:source) do
create(:tag_name, name: 'tmp').tap do |tn|
@@ -122,11 +121,15 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end
end
it 'deletes only the source and preserves the alias and its canonical tag' do
it 'deletes only the source and preserves the alias and its owning tag' do
described_class.apply!
expect(TagName.unscoped.exists?(source.id)).to be(false)
expect(alias_name.reload.canonical).to eq(existing_tag.tag_name)
expect(alias_name.reload).to have_attributes(
tag_id: existing_tag.id, language_code: 'ja', primary_flg: false)
expect(TagName.find_by!(tag_id: existing_tag.id,
language_code: 'ja', primary_flg: true))
.to eq(existing_tag.tag_name)
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
end
end
+33 -12
ファイルの表示
@@ -32,37 +32,58 @@ RSpec.describe TagName, type: :model do
expect(tag.reload.tag_name.reload.tag_id).to eq(tag.id)
expect(tag.tag_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)
end
it 'accepts an alias owned by the canonical tag' do
it 'persists an alias owned by the same tag as its primary name' do
tag = create(:tag)
alias_name = build(:tag_name, name: 'valid_alias', canonical: tag.tag_name,
tag:, primary_flg: false)
alias_name = create(:tag_name, :alias, name: 'valid_alias', tag:)
expect(alias_name).to have_attributes(
canonical_id: tag.tag_name_id, tag_id: tag.id,
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
primary_flg: false, language_code: 'ja')
expect(alias_name).to be_valid
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)
end
describe '.canonicalise' do
it 'resolves the primary name of the alias language on the same tag' do
tag = create(:tag, 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',
language_code: 'en', script_code: 'Latn')
representative_id = tag.tag_name_id
primary = described_class.find_by!(tag_id: english_alias.tag_id,
language_code: english_alias.language_code,
primary_flg: true)
expect(primary).to eq(english_primary)
expect(described_class.canonicalise(locale, [english_alias.name]))
.to eq([english_primary.name])
expect(tag.reload.tag_name_id).to eq(representative_id)
end
it 'resolves only aliases in the requested language' do
japanese = create(:tag, name: 'japanese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, tag_name: english_name)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
english_alias = create(:tag_name, :alias, name: 'temporary_alias',
canonical: english_name,
tag: japanese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
english_alias.update_columns(name: 'shared_alias')
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
canonical_id: english_name.id, tag_id: english.id,
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(described_class.canonicalise(locale, ['shared_alias']))
.to eq(['english_canonical'])
+27 -22
ファイルの表示
@@ -37,17 +37,16 @@ RSpec.describe Tag, type: :model do
language_code: 'en', script_code: 'Latn')
english = create(:tag, tag_name: english_name)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
english_alias = create(:tag_name, :alias, name: 'temporary_alias',
canonical: english_name,
tag: japanese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
english_alias.update_columns(name: 'shared_alias')
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
canonical_id: english_name.id, tag_id: english.id,
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(described_class.normalise_tags!(
english_locale, ['shared_alias'],
@@ -270,12 +269,12 @@ RSpec.describe Tag, type: :model do
expect(tag.category).to eq('character')
end
it 'reuses the canonical tag for an alias without changing its category' do
it 'reuses the owning tag for an alias without changing its category' do
tag = create(:tag, category: :character)
representative_id = tag.tag_name_id
alias_name = create(:tag_name, :alias, name: 'lookup_alias', canonical: tag.tag_name)
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
expect(alias_name.reload).to have_attributes(
canonical_id: tag.tag_name_id, tag_id: tag.id,
tag_id: tag.id,
primary_flg: false, language_code: 'ja')
found = nil
@@ -289,14 +288,14 @@ RSpec.describe Tag, type: :model do
expect(tag.reload.tag_name_id).to eq(representative_id)
end
it 'reuses the canonical tag through another alias' do
it 'reuses the owning tag through another alias' do
tag = create(:tag)
canonical = tag.tag_name
alias_name = create(:tag_name, :alias, name: 'lookup_alias', canonical:)
other_alias = create(:tag_name, :alias, name: 'lookup_other_alias', canonical:)
primary = tag.tag_name
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
other_alias = create(:tag_name, :alias, name: 'lookup_other_alias', tag:)
expect([alias_name.reload, other_alias.reload]).to all(have_attributes(
canonical_id: canonical.id, tag_id: tag.id,
tag_id: tag.id,
primary_flg: false, language_code: 'ja'))
expect {
@@ -305,7 +304,7 @@ 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.tag_name).to eq(canonical)
expect(tag.reload.tag_name).to eq(primary)
end
end
@@ -329,7 +328,8 @@ RSpec.describe Tag, type: :model do
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -355,7 +355,8 @@ RSpec.describe Tag, type: :model do
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -363,7 +364,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_alias = create(:tag_name, :alias, name: 'merge_alias', canonical: source_tag_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
@@ -433,7 +434,8 @@ RSpec.describe Tag, type: :model do
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -464,7 +466,8 @@ RSpec.describe Tag, type: :model do
expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -493,7 +496,8 @@ RSpec.describe Tag, type: :model do
}.to raise_error(ActiveRecord::RecordInvalid)
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
expect(earlier_name.reload.canonical_id).to be_nil
expect(earlier_name.reload).to have_attributes(
tag_id: earlier_source.id, language_code: 'ja', primary_flg: true)
expect(TagVersion.where(tag_id: [earlier_source.id, source_tag.id, target_tag.id]))
.to be_empty
expect(Tag.unscoped.exists?(source_tag.id)).to be(true)
@@ -502,7 +506,8 @@ RSpec.describe Tag, type: :model do
expect(source_post_tag.sections).to contain_exactly(source_section)
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
expect(source_tag.reload.post_count).to eq(1)
expect(source_tag_name.reload.canonical_id).to be_nil
expect(source_tag_name.reload).to have_attributes(
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
expect(target_tag.reload.post_count).to eq(0)
end
end
+9 -5
ファイルの表示
@@ -202,17 +202,19 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
expect(response).to have_http_status(:ok), response.body
expect(TagName.find_by!(language_code: 'en', name: 'english_alias'))
.to have_attributes(
script_code: 'Latn', canonical_id: english_name.id,
script_code: 'Latn',
tag_id: tag.id, primary_flg: false, language_code: 'en')
expect(tag.reload.tag_name_id).to eq(english_name.id)
primary = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
expect(primary).to have_attributes(name: 'english_original', script_code: 'Latn')
end
it 'does not take an alias from another language' do
japanese = create(:tag, name: 'japanese_owner')
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
tag: japanese)
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
put "/tags/#{ tag.id }", params: {
@@ -221,12 +223,14 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
expect(response).to have_http_status(:ok), response.body
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(TagName.find_by!(language_code: 'en', name: 'shared_alias'))
.to have_attributes(
canonical_id: english_name.id, tag_id: tag.id,
tag_id: tag.id, script_code: 'Latn',
primary_flg: false, language_code: 'en')
expect(TagName.where(name: 'shared_alias').pluck(:language_code, :tag_id))
.to contain_exactly(['ja', japanese.id], ['en', tag.id])
end
end
end
+4 -4
ファイルの表示
@@ -129,7 +129,7 @@ RSpec.describe 'Posts API', type: :request do
let!(:tag) { create(:tag, tag_name:, category: :general) }
let!(:tag_name2) { create(:tag_name, name: 'unko') }
let!(:tag2) { create(:tag, tag_name: tag_name2, category: :deerjikist) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', canonical: tag_name) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
let!(:hit_post) do
Post.create!(uploaded_user: user, title: 'hello spec world',
@@ -351,7 +351,7 @@ RSpec.describe 'Posts API', type: :request do
let!(:baz_tag) { create(:tag, tag_name: baz_tag_name, category: :general) }
let!(:foo_alias_tag_name) do
create(:tag_name, :alias, name: 'not_spec_foo_alias', canonical: foo_tag_name)
create(:tag_name, :alias, name: 'not_spec_foo_alias', tag: foo_tag)
end
let!(:foo_only_post) do
@@ -854,7 +854,7 @@ RSpec.describe 'Posts API', type: :request do
15.times.map do |i|
tag_name = create(:tag_name, name: "show_query_tag_#{ i }")
tag = create(:tag, tag_name:, category: :general)
create(:tag_name, :alias, name: "show_query_alias_#{ i }", canonical: tag_name)
create(:tag_name, :alias, name: "show_query_alias_#{ i }", tag:)
PostTag.create!(post: post_record, tag:)
tag
end
@@ -999,7 +999,7 @@ RSpec.describe 'Posts API', type: :request do
describe 'POST /posts' do
let(:member) { create(:user, :member) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', canonical: tag_name) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
it '401 when not logged in' do
sign_out
+50 -28
ファイルの表示
@@ -4,7 +4,7 @@ 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!(:alias_tn) { create(:tag_name, :alias, name: 'unko', canonical: tn) }
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') }
@@ -466,7 +466,7 @@ RSpec.describe 'Tags API', type: :request do
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)
create(:tag_name, :alias, name: 'switch_alias', canonical: alias_target.tag_name)
create(:tag_name, :alias, name: 'switch_alias', tag: alias_target)
create(:external_tag, name: 'switch_external', post_count: 1)
get '/tags/autocomplete', params: { q: 'switch', nico: '0' }
@@ -479,13 +479,13 @@ RSpec.describe 'Tags API', type: :request do
end
['%', '_'].each do |wildcard|
it "treats #{ wildcard } literally for canonical, alias, and external names" do
it "treats #{ wildcard } literally for primary, alias, and external names" do
literal = "literal#{ wildcard }match"
create(:tag, category: :general, name: literal, post_count: 1)
alias_target = create(
:tag, category: :general, name: 'literal_alias_target', post_count: 1)
create(:tag_name, :alias, name: "#{ literal }_alias",
canonical: alias_target.tag_name)
tag: alias_target)
create(:external_tag, name: literal, post_count: 1)
create(:tag, category: :general, name: 'literalXmatch', post_count: 2)
create(:external_tag, name: 'literalXmatch', post_count: 2)
@@ -509,7 +509,7 @@ RSpec.describe 'Tags API', type: :request do
expect(t['matched_alias']).to be(nil)
end
it 'returns matching canonical tags by q with aliases' do
it 'returns matching primary tag names by q with aliases' do
get '/tags/autocomplete', params: { q: 'unk' }
expect(response).to have_http_status(:ok)
@@ -772,7 +772,8 @@ RSpec.describe 'Tags API', type: :request do
}
expect(response).to have_http_status(:ok)
expect(TagName.find_by!(name: 'spec_tag').canonical).to eq(tag.reload.tag_name)
expect(TagName.find_by!(language_code: 'ja', name: 'spec_tag'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
patch "/tags/#{ tag.id }", params: { name: 'spec_tag' }
@@ -781,8 +782,10 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(tag.name).to eq('spec_tag')
expect(tag.tag_name.canonical_id).to be_nil
expect(TagName.find_by!(name: 'patch_roundtrip_target').canonical).to eq(tag.tag_name)
expect(tag.tag_name).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)
end
it '別 tag の正規名には変更できない' do
@@ -801,7 +804,8 @@ RSpec.describe 'Tags API', type: :request do
)
expect(tag.reload.name).to eq('spec_tag')
expect(tag.tag_name.aliases.map(&:name)).to contain_exactly('unko')
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)
end
@@ -1170,14 +1174,17 @@ RSpec.describe 'Tags API', type: :request do
expect(tag.name).to eq('put_renamed_tag')
expect(tag.category).to eq('meme')
expect(TagName.find_by(name: 'put_alias_a').canonical).to eq(tag.tag_name)
expect(TagName.find_by(name: 'put_alias_b').canonical).to eq(tag.tag_name)
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_a'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
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')
expect(old_name_alias).to be_present
expect(old_name_alias.canonical).to eq(tag.tag_name)
expect(old_name_alias).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(alias_tn.reload.canonical).to be_nil
expect(alias_tn.reload.tag_id).to be_nil
expect(tag.parents.map(&:name)).to contain_exactly(
'put_kept_parent',
@@ -1212,7 +1219,8 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(TagName.find_by(name: 'put_alias_self_test').canonical).to eq(tag.tag_name)
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_self_test'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(json['aliases']).to include('put_alias_self_test')
expect(json['aliases']).not_to include('spec_tag')
end
@@ -1283,10 +1291,14 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(tag.name).to eq('spec_tag')
expect(TagName.find_by!(name: 'put_roundtrip_b').canonical).to eq(tag.tag_name)
expect(tag.tag_name.aliases.map(&:name)).to contain_exactly('put_roundtrip_b', 'unko')
expect(tag.tag_name.aliases.map(&:name)).not_to include('spec_tag')
expect(alias_tn.reload.canonical).to eq(tag.tag_name)
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))
.to contain_exactly('put_roundtrip_b', 'unko')
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.not_to include('spec_tag')
expect(alias_tn.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
version = tag.tag_versions.order(:version_no).last
@@ -1395,7 +1407,7 @@ RSpec.describe 'Tags API', type: :request do
)
end
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新 canonical へ移す' do
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
@@ -1429,7 +1441,8 @@ RSpec.describe 'Tags API', type: :request do
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(TagName.find_by!(name: 'put_wiki_roundtrip_b').canonical).to eq(tag.tag_name)
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
versions = wiki_page.wiki_versions.order(:version_no).last(2)
@@ -1443,9 +1456,11 @@ RSpec.describe 'Tags API', type: :request do
category: :general)
stolen_alias = create(:tag_name, :alias,
name: 'put_stolen_alias',
canonical: old_owner.tag_name)
tag: old_owner)
expect(old_owner.tag_name.aliases.map(&:name)).to include('put_stolen_alias')
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.to include('put_stolen_alias')
expect {
put "/tags/#{ tag.id }", params: {
@@ -1461,8 +1476,11 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
expect(stolen_alias.reload.canonical).to eq(tag.tag_name)
expect(old_owner.reload.tag_name.aliases.map(&:name)).not_to include('put_stolen_alias')
expect(stolen_alias.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.not_to include('put_stolen_alias')
old_owner_versions = old_owner.tag_versions.order(:version_no)
@@ -1479,7 +1497,7 @@ RSpec.describe 'Tags API', type: :request do
category: :general)
stolen_alias = create(:tag_name, :alias,
name: 'put_alias_collision_name',
canonical: old_owner.tag_name)
tag: old_owner)
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
@@ -1502,9 +1520,13 @@ RSpec.describe 'Tags API', type: :request do
stolen_alias.reload
expect(tag.name).to eq('put_alias_collision_name')
expect(stolen_alias.canonical_id).to be_nil
expect(TagName.find_by!(name: 'spec_tag').canonical).to eq(tag.tag_name)
expect(old_owner.tag_name.aliases.map(&:name)).not_to include('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'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.not_to include('put_alias_collision_name')
old_owner_versions = old_owner.tag_versions.order(:version_no)
+8 -9
ファイルの表示
@@ -40,17 +40,16 @@ RSpec.describe PostCreatePlan do
language_code: 'en', script_code: 'Latn')
english = create(:tag, tag_name: english_name, category: :character)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
english_alias = create(:tag_name, :alias, name: 'temporary_alias',
canonical: english_name,
tag: japanese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
english_alias.update_columns(name: 'shared_alias')
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
canonical_id: english_name.id, tag_id: english.id,
tag_id: english.id,
primary_flg: false, language_code: 'en')
plan = described_class.new(attributes: { tags: 'shared_alias' }).build!(locale)
@@ -79,9 +78,9 @@ RSpec.describe PostCreatePlan do
expect([TagName.count, Tag.count]).to eq(counts)
end
it 'resolves aliases and keeps tag sections separate from canonical names' do
canonical = create_tag!('虹夏', :character)
create(:tag_name, :alias, name: 'にじか', canonical: canonical.tag_name)
it 'resolves aliases and keeps tag sections separate from primary names' do
tag = create_tag!('虹夏', :character)
create(:tag_name, :alias, name: 'にじか', tag:)
create_tag!('動画', :meta)
plan = described_class.new(