コミットを比較

..
8 コミット
作成者 SHA1 メッセージ 日付
みてるぞ 694ed60431 #411 2026-09-21 05:56:38 +09:00
みてるぞ 493ca749b4 #411 2026-09-21 05:46:02 +09:00
みてるぞ b217a307fe #411 2026-09-21 04:55:02 +09:00
みてるぞ 4b99743435 #411 化石ロジックを削除 2026-09-21 04:07:18 +09:00
みてるぞ 4fd95b74f1 #411 2026-09-21 03:57:16 +09:00
みてるぞ 3a5d79b5d7 #411 2026-09-21 03:49:46 +09:00
みてるぞ 6e33c5f192 #411 2026-09-21 03:30:22 +09:00
みてるぞ 2425f1af1f #411 2026-09-21 02:55:00 +09:00
27個のファイルの変更117行の追加621行の削除
-2
ファイルの表示
@@ -92,6 +92,4 @@ class ApplicationController < ActionController::API
value value
end end
def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese
end end
+5 -4
ファイルの表示
@@ -72,7 +72,6 @@ class MaterialsController < ApplicationController
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member? return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
tag_name_raw = params[:tag].to_s.strip tag_name_raw = params[:tag].to_s.strip
file = params[:file] file = params[:file]
file_sha256 = MaterialFileSha256.from_upload(file) file_sha256 = MaterialFileSha256.from_upload(file)
@@ -90,7 +89,7 @@ class MaterialsController < ApplicationController
begin begin
Material.transaction do Material.transaction do
tag = resolve_material_tag!(locale, tag_name_raw) tag = resolve_material_tag!(tag_name_raw)
material = Material.new(tag:, url:, material = Material.new(tag:, url:,
created_by_user: current_user, created_by_user: current_user,
updated_by_user: current_user) updated_by_user: current_user)
@@ -237,8 +236,10 @@ class MaterialsController < ApplicationController
nil nil
end end
def resolve_material_tag! locale, tag_name_raw def resolve_material_tag! tag_name_raw
Tag.find_or_create_by_tag_name!(locale, tag_name_raw, category: :material) tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
tag = tag_name.tag
tag || Tag.create!(tag_name:, category: :material)
end end
def material_index_needs_tag_name? filters def material_index_needs_tag_name? filters
+2 -5
ファイルの表示
@@ -70,8 +70,6 @@ class NicoTagsController < ApplicationController
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member? return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
id = params[:id].to_i id = params[:id].to_i
tag = Tag.find(id) tag = Tag.find(id)
@@ -81,9 +79,8 @@ class NicoTagsController < ApplicationController
linked_tags = nil linked_tags = nil
ApplicationRecord.transaction do ApplicationRecord.transaction do
linked_tags = Tag.normalise_tags!(locale, linked_tag_names, linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false,
with_tagme: false, with_no_deerjikist: false)
with_no_deerjikist: false)
if linked_tags.any? { |t| t.nico? } if linked_tags.any? { |t| t.nico? }
raise Tag::NicoTagNormalisationError raise Tag::NicoTagNormalisationError
end end
+11 -14
ファイルの表示
@@ -308,7 +308,6 @@ class PostsController < ApplicationController
base_version_no = parse_base_version_no base_version_no = parse_base_version_no
return render_bad_request('base_version_no は必須です.') if !(force) && !(base_version_no) return render_bad_request('base_version_no は必須です.') if !(force) && !(base_version_no)
locale = Locale.find_by(code: params[:locale].presence) || Locale.nipponese
title = params[:title].presence title = params[:title].presence
tag_names = params[:tags].to_s.split tag_names = params[:tags].to_s.split
original_created_from = params[:original_created_from] original_created_from = params[:original_created_from]
@@ -330,8 +329,7 @@ class PostsController < ApplicationController
base_snapshot = post_snapshot_from_version(base_version) base_snapshot = post_snapshot_from_version(base_version)
current_snapshot = post_snapshot_from_record(post) current_snapshot = post_snapshot_from_record(post)
end end
incoming_snapshot = post_incoming_snapshot(locale:, incoming_snapshot = post_incoming_snapshot(title:,
title:,
original_created_from:, original_created_from:,
original_created_before:, original_created_before:,
tag_names:, tag_names:,
@@ -360,7 +358,7 @@ class PostsController < ApplicationController
end end
end end
apply_post_snapshot!(locale, post, snapshot_to_apply) apply_post_snapshot!(post, snapshot_to_apply)
end end
return render json: conflict_json, status: :conflict if conflict_json return render json: conflict_json, status: :conflict if conflict_json
@@ -687,13 +685,12 @@ class PostsController < ApplicationController
end end
end end
def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:, def post_incoming_snapshot title:, original_created_from:, original_created_before:,
tag_names:, video_ms_param:, duration_param:, parent_post_ids: tag_names:, video_ms_param:, duration_param:, parent_post_ids:
validate_original_created_values!(original_created_from, original_created_before) validate_original_created_values!(original_created_from, original_created_before)
Tag.normalise_tags!(locale, tag_names, Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
with_tagme: false, with_sections: true) =>
deny_deprecated: true, { tags:, sections: }
with_sections: true) => { tags:, sections: }
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?) tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:) video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:)
@@ -828,7 +825,7 @@ class PostsController < ApplicationController
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present? (added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
end end
def apply_post_snapshot! locale, post, snapshot def apply_post_snapshot! post, snapshot
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user) PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
post.update!(title: snapshot[:title], post.update!(title: snapshot[:title],
@@ -836,10 +833,10 @@ class PostsController < ApplicationController
original_created_from: snapshot[:original_created_from], original_created_from: snapshot[:original_created_from],
original_created_before: snapshot[:original_created_before]) original_created_before: snapshot[:original_created_before])
Tag.normalise_tags!(locale, snapshot[:tag_names], Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false,
with_tagme: false, deny_deprecated: true,
deny_deprecated: true, with_sections: true) =>
with_sections: true) => { tags: editable_tags, sections: } { tags: editable_tags, sections: }
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user) TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
readonly_tags = post.tags.nico.to_a readonly_tags = post.tags.nico.to_a
+16 -14
ファイルの表示
@@ -278,8 +278,6 @@ class TagsController < ApplicationController
return unless validate_tag_rename(tag, name) return unless validate_tag_rename(tag, name)
locale = resolve_locale!
alias_names = params[:aliases].to_s.split.uniq alias_names = params[:aliases].to_s.split.uniq
parent_names = params[:parent_tags].to_s.split.uniq parent_names = params[:parent_tags].to_s.split.uniq
deprecated = bool?(:deprecated) deprecated = bool?(:deprecated)
@@ -310,7 +308,7 @@ class TagsController < ApplicationController
alias_names.delete(name) alias_names.delete(name)
update_aliases!(tag, alias_names) update_aliases!(tag, alias_names)
update_parent_tags!(locale, tag, parent_names) update_parent_tags!(tag, parent_names)
tag.reload tag.reload
@@ -575,7 +573,7 @@ class TagsController < ApplicationController
return false return false
end end
target_tag_name = TagName.find_by(name:) target_tag_name = TagName.with_discarded.find_by(name:)
return true if target_tag_name.nil? return true if target_tag_name.nil?
return true if target_tag_name.canonical_id? return true if target_tag_name.canonical_id?
@@ -587,14 +585,17 @@ class TagsController < ApplicationController
return if name == tag.name return if name == tag.name
current_tag_name = tag.tag_name current_tag_name = tag.tag_name
target_tag_name = TagName.find_by(name:) target_tag_name = TagName.with_discarded.find_by(name:)
if target_tag_name.nil? if target_tag_name.nil?
current_tag_name.update!(name:) current_tag_name.update!(name:)
return return
end end
promote_tag_alias!(tag, current_tag_name:, promoted_tag_name: target_tag_name) promote_tag_alias!(
tag,
current_tag_name:,
promoted_tag_name: target_tag_name)
end end
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name: def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
@@ -604,9 +605,11 @@ class TagsController < ApplicationController
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user) TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
end end
promoted_tag_name.undiscard! if promoted_tag_name.discarded?
promoted_tag_name.update!(canonical: nil) promoted_tag_name.update!(canonical: nil)
TagName.where(canonical_id: current_tag_name.id) TagName.with_discarded
.where(canonical_id: current_tag_name.id)
.where.not(id: promoted_tag_name.id) .where.not(id: promoted_tag_name.id)
.find_each do |alias_tag_name| .find_each do |alias_tag_name|
alias_tag_name.update!(canonical: promoted_tag_name) alias_tag_name.update!(canonical: promoted_tag_name)
@@ -637,7 +640,7 @@ class TagsController < ApplicationController
end end
alias_names.each do |alias_name| alias_names.each do |alias_name|
alias_tag_name = TagName.find_or_create_by!(name: alias_name) alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
affected_tags << alias_tag_name.canonical&.tag affected_tags << alias_tag_name.canonical&.tag
end end
@@ -652,7 +655,7 @@ class TagsController < ApplicationController
end end
alias_names.each do |alias_name| alias_names.each do |alias_name|
alias_tag_name = TagName.find_or_create_by!(name: alias_name) alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
alias_tag_name.update!(canonical: tag.tag_name) alias_tag_name.update!(canonical: tag.tag_name)
end end
@@ -661,11 +664,10 @@ class TagsController < ApplicationController
end end
end end
def update_parent_tags! locale, tag, parent_names def update_parent_tags! tag, parent_names
parent_tags = Tag.normalise_tags!(locale, parent_names, parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false,
with_tagme: false, with_no_deerjikist: false,
with_no_deerjikist: false, deny_nico: true)
deny_nico: true)
old_parent_tags = tag.parents.to_a old_parent_tags = tag.parents.to_a
+1 -1
ファイルの表示
@@ -94,7 +94,7 @@ class WikiPagesController < ApplicationController
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank? return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank? return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
tag_name = TagName.find_or_create_by!(name: title) tag_name = TagName.find_undiscard_or_create_by!(name: title)
page = page =
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
-3
ファイルの表示
@@ -1,3 +0,0 @@
class Language < ApplicationRecord
has_many :languages, class_name: 'Locale', foreign_key: :language_code
end
-24
ファイルの表示
@@ -1,24 +0,0 @@
class Locale < ApplicationRecord
after_create_commit :generate_tag_names!
belongs_to :language, foreign_key: :language_code, primary_key: :code
belongs_to :script, foreign_key: :script_code, primary_key: :code
def self.nipponese = Locale.find('ja')
private
def generate_tag_names!
tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id)
Tag.where.not(id: tag_ids).find_each do
TagName.create!(tag_id: _1.id,
language_code:,
name: TagName.generate_name(self, _1, _1.name),
script_code:,
primary_flg: true,
# TODO: 公証実装したら書く.
# auto_generated: true,
canonical_id: nil)
end
end
end
-3
ファイルの表示
@@ -63,19 +63,16 @@ class Post < ApplicationRecord
has_many :user_post_views, dependent: :delete_all has_many :user_post_views, dependent: :delete_all
has_many :post_similarities, dependent: :delete_all has_many :post_similarities, dependent: :delete_all
has_many :post_versions has_many :post_versions
has_many :gekanator_guessed_games, has_many :gekanator_guessed_games,
class_name: 'GekanatorGame', class_name: 'GekanatorGame',
foreign_key: :guessed_post_id, foreign_key: :guessed_post_id,
dependent: :delete_all, dependent: :delete_all,
inverse_of: :guessed_post inverse_of: :guessed_post
has_many :gekanator_correct_games, has_many :gekanator_correct_games,
class_name: 'GekanatorGame', class_name: 'GekanatorGame',
foreign_key: :correct_post_id, foreign_key: :correct_post_id,
dependent: :delete_all, dependent: :delete_all,
inverse_of: :correct_post inverse_of: :correct_post
has_many :gekanator_question_examples, dependent: :delete_all has_many :gekanator_question_examples, dependent: :delete_all
has_many :parent_post_implications, has_many :parent_post_implications,
-3
ファイルの表示
@@ -1,3 +0,0 @@
class Script < ApplicationRecord
;
end
+22 -35
ファイルの表示
@@ -2,6 +2,8 @@ require 'set'
class Tag < ApplicationRecord class Tag < ApplicationRecord
include MyDiscard
class NicoTagNormalisationError < ArgumentError class NicoTagNormalisationError < ArgumentError
; ;
end end
@@ -52,7 +54,6 @@ class Tag < ApplicationRecord
has_many :tag_versions has_many :tag_versions
has_many :nico_tag_versions has_many :nico_tag_versions
has_many :tag_names
belongs_to :tag_name belongs_to :tag_name
delegate :wiki_page, to: :tag_name delegate :wiki_page, to: :tag_name
@@ -102,20 +103,18 @@ class Tag < ApplicationRecord
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists? def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
def self.tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta) def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
def self.bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta) def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
def self.no_deerjikist = def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta) def self.video = find_or_create_by_tag_name!('動画', category: :meta)
def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta) def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
def self.niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta) def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
def self.youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
def self.normalise_tags! locale, tag_names, def self.normalise_tags! tag_names, with_tagme: true,
with_tagme: true, with_no_deerjikist: true,
with_no_deerjikist: true, deny_nico: true,
deny_nico: true, deny_deprecated: false,
deny_deprecated: false, with_sections: false
with_sections: false
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') } if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
raise NicoTagNormalisationError raise NicoTagNormalisationError
end end
@@ -144,7 +143,7 @@ class Tag < ApplicationRecord
name = TagName.canonicalise(name).first name = TagName.canonicalise(name).first
find_or_create_by_tag_name!(locale, name, category: (cat || :general)).tap do |tag| find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
if deny_deprecated && tag.deprecated? if deny_deprecated && tag.deprecated?
raise DeprecatedTagNormalisationError, [tag.name] raise DeprecatedTagNormalisationError, [tag.name]
end end
@@ -232,25 +231,13 @@ class Tag < ApplicationRecord
[left_end_ms, right_end_ms].max [left_end_ms, right_end_ms].max
end end
def self.find_or_create_by_tag_name! locale, name, category: def self.find_or_create_by_tag_name! name, category:
language_code = locale.language_code tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
name = name.to_s.strip tn = tn.canonical if tn.canonical_id?
tn = TagName.find_or_create_by!(language_code:, name:) do Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
_1.script_code = locale.script_code t.category = category
_1.primary_flg = true
end end
tag = tn.tag
if tag
tag.update!(tag_name_id: tn.id) if tag.tag_name_id != tn.id
return tag
end
tag = Tag.create!(tag_name: tn, category:)
tn.update!(tag:)
tag
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
retry retry
end end
@@ -286,10 +273,10 @@ class Tag < ApplicationRecord
end end
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:) TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
source_tag.destroy! source_tag.discard!
if source_tag.nico? if source_tag.nico?
source_tag_name.destroy! source_tag_name.discard!
else else
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id, source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
updated_at: Time.current) updated_at: Time.current)
@@ -310,7 +297,7 @@ class Tag < ApplicationRecord
target_tag.reload target_tag.reload
end end
def snapshot_aliases = tag_name.aliases.order(:name).pluck(:name) def snapshot_aliases = tag_name.aliases.kept.order(:name).pluck(:name)
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id) def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
+3 -7
ファイルの表示
@@ -1,5 +1,7 @@
class TagName < ApplicationRecord class TagName < ApplicationRecord
belongs_to :tag, optional: true include MyDiscard
has_one :tag
has_one :wiki_page has_one :wiki_page
belongs_to :canonical, class_name: 'TagName', optional: true belongs_to :canonical, class_name: 'TagName', optional: true
@@ -21,12 +23,6 @@ class TagName < ApplicationRecord
names.map { |name| tns[name]&.canonical&.name || name }.uniq names.map { |name| tns[name]&.canonical&.name || name }.uniq
end end
def self.generate_name locale, tag, name
# TODO: 言語ごとの自動命名ロジック完成したら書く.
"Tag_##{ tag.id }"
end
private private
def canonical_must_be_canonical def canonical_must_be_canonical
+1 -1
ファイルの表示
@@ -32,7 +32,7 @@ class TagNameSanitisationRule < ApplicationRecord
elsif source_tag elsif source_tag
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current) source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
end end
tn.destroy! tn.discard!
next next
end end
+9 -9
ファイルの表示
@@ -9,7 +9,7 @@ class PostCreator
@field_warnings = { } @field_warnings = { }
end end
def create!(locale) def create!
thumbnail_attachment = prepare_thumbnail_attachment thumbnail_attachment = prepare_thumbnail_attachment
post = Post.new(title: @attributes[:title].presence, post = Post.new(title: @attributes[:title].presence,
url: @attributes[:url], url: @attributes[:url],
@@ -25,7 +25,7 @@ class PostCreator
post_tags = planned_post_tags post_tags = planned_post_tags
sections = planned_sections sections = planned_sections
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor) TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
post.video_ms = planned_video_ms(locale) post.video_ms = planned_video_ms
post.save! post.save!
sync_post_tags!(post, post_tags, sections) sync_post_tags!(post, post_tags, sections)
sync_parent_posts!(post, planned_parent_post_ids) sync_parent_posts!(post, planned_parent_post_ids)
@@ -53,15 +53,15 @@ class PostCreator
def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids] def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]
def planned_video_ms locale def planned_video_ms
planned_create_attributes(locale)[:video_ms] planned_create_attributes[:video_ms]
end end
def planned_create_attributes locale def planned_create_attributes
@planned_create_attributes ||= begin @planned_create_attributes ||= begin
if @attributes.key?(:snapshot_tag_specs) if @attributes.key?(:snapshot_tag_specs)
snapshot_tags = materialise_tags(locale, @attributes[:snapshot_tag_specs] || []) snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
post_tags = materialise_tags(locale, @attributes[:post_tag_specs] || []) post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
{ {
snapshot_tags: snapshot_tags, snapshot_tags: snapshot_tags,
post_tags: post_tags, post_tags: post_tags,
@@ -92,13 +92,13 @@ class PostCreator
video_ms: plan[:video_ms] } video_ms: plan[:video_ms] }
end end
def materialise_tags locale, specs def materialise_tags specs
Array(specs).each_with_object({ }) do |spec, tags| Array(specs).each_with_object({ }) do |spec, tags|
name = spec[:name] || spec['name'] name = spec[:name] || spec['name']
category = spec[:category] || spec['category'] category = spec[:category] || spec['category']
next if name.blank? || category.blank? next if name.blank? || category.blank?
tag = Tag.find_or_create_by_tag_name!(locale, name, category:) tag = Tag.find_or_create_by_tag_name!(name, category:)
tag.update!(category:) if tag.category.to_sym != category.to_sym tag.update!(category:) if tag.category.to_sym != category.to_sym
tags[name] ||= tag tags[name] ||= tag
end.values end.values
-46
ファイルの表示
@@ -1,46 +0,0 @@
class DeleteDiscardedRecordsFromTags < ActiveRecord::Migration[8.0]
def up
remove_foreign_key :tag_versions, :tags, column: :tag_id
remove_foreign_key :nico_tag_versions, :tags, column: :tag_id
remove_foreign_key :material_versions, :tags, column: :tag_id
execute <<~SQL
DELETE
ntr
FROM
nico_tag_relations ntr
INNER JOIN
tags t
ON
t.discarded_at IS NOT NULL
AND t.id IN (ntr.tag_id, ntr.nico_tag_id)
SQL
execute <<~SQL
DELETE
ti
FROM
tag_implications ti
INNER JOIN
tags t
ON
t.discarded_at IS NOT NULL
AND t.id IN (ti.tag_id, ti.parent_tag_id)
SQL
execute <<~SQL
DELETE
FROM
tags
WHERE
discarded_at IS NOT NULL
SQL
remove_index :tags, :discarded_at
remove_column :tags, :discarded_at
end
def down
raise ActiveRecord::IrreversibleMigration, '戻せません.'
end
end
-18
ファイルの表示
@@ -1,18 +0,0 @@
class DeleteDiscardedRecordsFromTagNames < ActiveRecord::Migration[8.0]
def up
execute <<~SQL
DELETE
FROM
tag_names
WHERE
discarded_at IS NOT NULL
SQL
remove_index :tag_names, :discarded_at
remove_column :tag_names, :discarded_at
end
def down
raise ActiveRecord::IrreversibleMigration, '戻せません.'
end
end
-20
ファイルの表示
@@ -1,20 +0,0 @@
class CreateLanguages < ActiveRecord::Migration[8.0]
def up
create_table :languages, id: { type: :string, limit: 16 }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
end
execute <<~SQL
INSERT INTO
languages(code, name, created_at)
VALUES
('ja', '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :languages
end
end
-20
ファイルの表示
@@ -1,20 +0,0 @@
class CreateScripts < ActiveRecord::Migration[8.0]
def up
create_table :scripts, id: { type: 'CHAR(4)' }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
end
execute <<~SQL
INSERT INTO
scripts(code, name, created_at)
VALUES
('Jpan', '漢字および仮名文字', #{ connection.quote Time.current })
SQL
end
def down
drop_table :scripts
end
end
-25
ファイルの表示
@@ -1,25 +0,0 @@
class CreateLocales < ActiveRecord::Migration[8.0]
def up
create_table :locales, id: { type: :string, limit: 32 }, primary_key: :code do |t|
t.string :language_code, limit: 16, null: false, index: true
t.column :script_code, 'CHAR(4)', null: false
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
t.foreign_key :languages, column: :language_code, primary_key: :code
t.foreign_key :scripts, column: :script_code, primary_key: :code
end
execute <<~SQL
INSERT INTO
locales(code, language_code, script_code, name, created_at)
VALUES
('ja', 'ja', 'Jpan', '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :locales
end
end
-59
ファイルの表示
@@ -1,59 +0,0 @@
class AddColumnsToTagNames < ActiveRecord::Migration[8.0]
def up
add_reference :tag_names, :tag, after: :id, foreign_key: true
add_column :tag_names, :language_code, :string,
limit: 16, null: false, after: :tag_id, default: 'ja'
add_column :tag_names, :script_code, 'CHAR(4)',
null: false, after: :name, default: 'Jpan'
add_column :tag_names, :primary_flg, :boolean,
null: false, after: :script_code, default: true
add_foreign_key :tag_names, :languages, column: :language_code, primary_key: :code
add_foreign_key :tag_names, :scripts, column: :script_code, primary_key: :code
remove_index :tag_names, :name
add_index :tag_names, [:language_code, :name], unique: true
change_column_default :tag_names, :language_code, from: 'ja', to: nil
change_column_default :tag_names, :script_code, from: 'Jpan', to: nil
execute <<~SQL
UPDATE
tag_names tn
LEFT JOIN
tags AS t
ON
t.tag_name_id = COALESCE(tn.canonical_id, tn.id)
SET
tn.tag_id = t.id
, tn.primary_flg = CASE
WHEN tn.canonical_id IS NULL THEN
1
ELSE
0
END
SQL
change_column_default :tag_names, :primary_flg, from: true, to: nil
add_column :tag_names, :primary_tag_id, :bigint,
as: 'CASE WHEN primary_flg THEN tag_id ELSE NULL END'
add_index :tag_names, [:primary_tag_id, :language_code], unique: true
end
def down
remove_index :tag_names, [:primary_tag_id, :language_code]
remove_column :tag_names, :primary_tag_id
remove_foreign_key :tag_names, column: :language_code
remove_foreign_key :tag_names, column: :script_code
remove_index :tag_names, [:language_code, :name]
add_index :tag_names, :name, unique: true
remove_column :tag_names, :primary_flg
remove_column :tag_names, :script_code
remove_column :tag_names, :language_code
remove_reference :tag_names, :tag, foreign_key: true
end
end
生成ファイル
+8 -1
ファイルの表示
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@@ -409,7 +409,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
t.bigint "canonical_id" t.bigint "canonical_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.datetime "discarded_at"
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id" t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
t.index ["discarded_at"], name: "index_tag_names_on_discarded_at"
t.index ["name"], name: "index_tag_names_on_name", unique: true t.index ["name"], name: "index_tag_names_on_name", unique: true
end end
@@ -446,8 +448,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "post_count", default: 0, null: false t.integer "post_count", default: 0, null: false
t.datetime "discarded_at"
t.integer "version_no", null: false t.integer "version_no", null: false
t.index ["deprecated_at"], name: "index_tags_on_deprecated_at" t.index ["deprecated_at"], name: "index_tags_on_deprecated_at"
t.index ["discarded_at"], name: "index_tags_on_discarded_at"
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
t.check_constraint "(`deprecated_at` is null) or (`category` <> _utf8mb4'nico')", name: "chk_tags_deprecated_at_not_nico" t.check_constraint "(`deprecated_at` is null) or (`category` <> _utf8mb4'nico')", name: "chk_tags_deprecated_at_not_nico"
t.check_constraint "`version_no` > 0", name: "chk_tags_version_no_positive" t.check_constraint "`version_no` > 0", name: "chk_tags_version_no_positive"
@@ -694,6 +698,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id" add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id"
add_foreign_key "material_versions", "materials" add_foreign_key "material_versions", "materials"
add_foreign_key "material_versions", "materials", column: "parent_id" add_foreign_key "material_versions", "materials", column: "parent_id"
add_foreign_key "material_versions", "tags"
add_foreign_key "material_versions", "users", column: "created_by_user_id" add_foreign_key "material_versions", "users", column: "created_by_user_id"
add_foreign_key "material_versions", "users", column: "updated_by_user_id" add_foreign_key "material_versions", "users", column: "updated_by_user_id"
add_foreign_key "materials", "materials", column: "parent_id" add_foreign_key "materials", "materials", column: "parent_id"
@@ -702,6 +707,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
add_foreign_key "materials", "users", column: "updated_by_user_id" add_foreign_key "materials", "users", column: "updated_by_user_id"
add_foreign_key "nico_tag_relations", "tags" add_foreign_key "nico_tag_relations", "tags"
add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id" add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id"
add_foreign_key "nico_tag_versions", "tags"
add_foreign_key "nico_tag_versions", "users", column: "created_by_user_id" add_foreign_key "nico_tag_versions", "users", column: "created_by_user_id"
add_foreign_key "post_implications", "posts" add_foreign_key "post_implications", "posts"
add_foreign_key "post_implications", "posts", column: "parent_post_id" add_foreign_key "post_implications", "posts", column: "parent_post_id"
@@ -720,6 +726,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
add_foreign_key "tag_names", "tag_names", column: "canonical_id" add_foreign_key "tag_names", "tag_names", column: "canonical_id"
add_foreign_key "tag_similarities", "tags" add_foreign_key "tag_similarities", "tags"
add_foreign_key "tag_similarities", "tags", column: "target_tag_id" add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
add_foreign_key "tag_versions", "tags"
add_foreign_key "tag_versions", "users", column: "created_by_user_id" add_foreign_key "tag_versions", "users", column: "created_by_user_id"
add_foreign_key "tags", "tag_names" add_foreign_key "tags", "tag_names"
add_foreign_key "theatre_comments", "theatres" add_foreign_key "theatre_comments", "theatres"
+1 -1
ファイルの表示
@@ -124,7 +124,7 @@ namespace :nico do
datum['tags'].each do |raw| datum['tags'].each do |raw|
name = TagNameSanitisationRule.sanitise("nico:#{ raw }") name = TagNameSanitisationRule.sanitise("nico:#{ raw }")
tag = Tag.find_or_create_by_tag_name!(Locale.nipponese, name, category: :nico) tag = Tag.find_or_create_by_tag_name!(name, category: :nico)
event_type = tag.nico_tag_versions.exists? ? :update : :create event_type = tag.nico_tag_versions.exists? ? :update : :create
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil) NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil)
-123
ファイルの表示
@@ -1,123 +0,0 @@
require 'rails_helper'
require_relative '../../db/migrate/20260921020000_delete_discarded_records_from_tags'
require_relative '../../db/migrate/20260921030000_delete_discarded_records_from_tag_names'
RSpec.describe 'discarded tag cleanup migrations' do
[DeleteDiscardedRecordsFromTags, DeleteDiscardedRecordsFromTagNames].each do |migration_class|
it "rejects rollback of #{ migration_class.name }" do
expect { migration_class.new.down }
.to raise_error(ActiveRecord::IrreversibleMigration)
end
end
context 'with legacy records' do
self.use_transactional_tests = false
before do
record_class = Class.new(ActiveRecord::Base) do
self.abstract_class = true
end
stub_const('TagCleanupMigrationRecord', record_class)
config = ActiveRecord::Base.connection_db_config.configuration_hash
@database = "btrc_hub_test_tag_cleanup_#{ Process.pid }_#{ SecureRandom.hex(4) }"
record_class.establish_connection(config.merge(database: nil))
@connection = record_class.lease_connection
@connection.create_database(@database)
@database_created = true
@connection.execute("USE #{ @connection.quote_table_name(@database) }")
end
after do
@connection.drop_database(@database) if @database_created
ensure
TagCleanupMigrationRecord.remove_connection
end
before do
@connection.create_table(:tag_names) do |t|
t.string :name, null: false, index: { unique: true }
t.bigint :canonical_id
t.datetime :discarded_at, index: true
end
@connection.add_foreign_key(:tag_names, :tag_names, column: :canonical_id)
@connection.create_table(:tags) do |t|
t.references :tag_name, null: false, foreign_key: true, index: { unique: true }
t.datetime :discarded_at, index: true
end
@connection.create_table(:nico_tag_relations) do |t|
t.references :tag, null: false, foreign_key: true
t.references :nico_tag, null: false, foreign_key: { to_table: :tags }
end
@connection.create_table(:tag_implications) do |t|
t.references :tag, null: false, foreign_key: true
t.references :parent_tag, null: false, foreign_key: { to_table: :tags }
end
[:tag_versions, :nico_tag_versions, :material_versions].each do |table|
@connection.create_table(table) do |t|
t.references :tag, null: false, foreign_key: true
end
end
@connection.execute(<<~SQL)
INSERT INTO tag_names (id, name, canonical_id, discarded_at) VALUES
(1, 'kept', NULL, NULL),
(2, 'merged_alias', 1, NULL),
(3, 'nico:deleted', NULL, '2026-09-20'),
(4, 'nico:kept', NULL, NULL),
(5, 'deleted_name', NULL, '2026-09-20')
SQL
@connection.execute(<<~SQL)
INSERT INTO tags (id, tag_name_id, discarded_at) VALUES
(1, 1, NULL), (2, 2, '2026-09-20'),
(3, 3, '2026-09-20'), (4, 4, NULL)
SQL
@connection.execute(<<~SQL)
INSERT INTO nico_tag_relations (id, tag_id, nico_tag_id) VALUES
(1, 1, 4), (2, 2, 4), (3, 1, 3), (4, 2, 3)
SQL
@connection.execute(<<~SQL)
INSERT INTO tag_implications (id, tag_id, parent_tag_id) VALUES
(1, 1, 4), (2, 2, 1), (3, 1, 2), (4, 2, 3)
SQL
@connection.execute('INSERT INTO tag_versions (tag_id) VALUES (1), (2)')
@connection.execute('INSERT INTO nico_tag_versions (tag_id) VALUES (3), (4)')
@connection.execute('INSERT INTO material_versions (tag_id) VALUES (1), (2)')
end
it 'removes discarded records and their links while retaining aliases and history' do
[DeleteDiscardedRecordsFromTags, DeleteDiscardedRecordsFromTagNames].each do |klass|
migration = klass.new
allow(migration).to receive(:connection).and_return(@connection)
migration.suppress_messages { migration.up }
end
expect(@connection.select_values('SELECT id FROM tags ORDER BY id')).to eq([1, 4])
expect(@connection.select_rows('SELECT id, canonical_id FROM tag_names ORDER BY id'))
.to eq([[1, nil], [2, 1], [4, nil]])
expect(@connection.select_values('SELECT id FROM nico_tag_relations')).to eq([1])
expect(@connection.select_values('SELECT id FROM tag_implications')).to eq([1])
expect(@connection.select_values('SELECT tag_id FROM tag_versions ORDER BY tag_id'))
.to eq([1, 2])
expect(@connection.select_values('SELECT tag_id FROM nico_tag_versions ORDER BY tag_id'))
.to eq([3, 4])
expect(@connection.select_values('SELECT tag_id FROM material_versions ORDER BY tag_id'))
.to eq([1, 2])
[:tags, :tag_names].each do |table|
expect(@connection.column_exists?(table, :discarded_at)).to be(false)
expect(@connection.index_exists?(table, :discarded_at)).to be(false)
end
[:tag_versions, :nico_tag_versions, :material_versions].each do |table|
expect(@connection.foreign_key_exists?(table, :tags, column: :tag_id)).to be(false)
end
expect(@connection.foreign_key_exists?(:tags, :tag_names)).to be(true)
expect(@connection.foreign_key_exists?(:tag_names, :tag_names, column: :canonical_id))
.to be(true)
expect(@connection.index_exists?(:tag_names, :name, unique: true)).to be(true)
expect(@connection.index_exists?(:tags, :tag_name_id, unique: true)).to be(true)
[:nico_tag_relations, :tag_implications].each do |table|
expect(@connection.foreign_keys(table).map(&:to_table)).to eq(['tags', 'tags'])
end
end
end
end
+6 -28
ファイルの表示
@@ -57,7 +57,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
it 'deletes the source tag_name' do it 'deletes the source tag_name' do
described_class.apply! described_class.apply!
expect(TagName.unscoped.exists?(source.id)).to be(false) expect(TagName.exists?(source.id)).to be(false)
expect(existing.reload.name).to eq('foobar') expect(existing.reload.name).to eq('foobar')
end end
end end
@@ -75,27 +75,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
described_class.apply! described_class.apply!
expected_tag_name_id = existing.canonical_id || existing.id 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(expected_tag_name_id)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false) expect(TagName.exists?(source_tag_name_id)).to be(false)
end
end
context 'when the sanitised name is an alias of an existing tag' do
let!(:existing_tag) { create(:tag) }
let!(:alias_name) do
TagName.create!(name: 'foobar', canonical: existing_tag.tag_name)
end
let!(:source) do
TagName.create!(name: 'tmp').tap do |tn|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end
end
it 'deletes only the source and preserves the alias and its canonical tag' do
described_class.apply!
expect(TagName.unscoped.exists?(source.id)).to be(false)
expect(alias_name.reload.canonical).to eq(existing_tag.tag_name)
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
end end
end end
@@ -112,15 +92,13 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end end
it 'merges the source tag into the existing tag and deletes the source tag_name' do it 'merges the source tag into the existing tag and deletes the source tag_name' do
post = create(:post) expect(TagName.find_by(name: 'foobar')&.tag&.id).to eq(existing_tag.id)
PostTag.create!(post:, tag: source_tag) expect(TagName.find_by(name: 'foo_bar')&.tag&.id).to eq(source_tag.id)
described_class.apply! described_class.apply!
expect(Tag.unscoped.exists?(source_tag.id)).to be(false) expect(Tag.exists?(source_tag.id)).to be(false)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false) expect(TagName.exists?(source_tag.tag_name_id)).to be(false)
expect(post.reload.tags).to contain_exactly(existing_tag)
expect(existing_tag.reload.name).to eq('foobar')
end end
end end
end end
+24 -152
ファイルの表示
@@ -173,47 +173,6 @@ RSpec.describe Tag, type: :model do
end end
end end
describe '.find_or_create_by_tag_name!' do
it 'creates a tag and name with the requested category after stripping whitespace' do
tag = nil
expect {
tag = described_class.find_or_create_by_tag_name!(
' lookup_new ', category: :character)
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
expect(tag.name).to eq('lookup_new')
expect(tag.category).to eq('character')
end
it 'reuses the canonical tag for an alias without changing its category' do
tag = create(:tag, category: :character)
alias_name = TagName.create!(name: 'lookup_alias', canonical: tag.tag_name)
found = nil
expect {
found = described_class.find_or_create_by_tag_name!(
alias_name.name, category: :general)
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
expect(found).to eq(tag)
expect(found.category).to eq('character')
end
it 'creates a tag for an existing canonical name reached through an alias' do
canonical = create(:tag_name)
alias_name = TagName.create!(name: 'lookup_alias', canonical:)
tag = nil
expect {
tag = described_class.find_or_create_by_tag_name!(
alias_name.name, category: :general)
}.to change(Tag, :count).by(1).and change(TagName, :count).by(0)
expect(tag.tag_name).to eq(canonical)
end
end
describe '.merge_tags!' do describe '.merge_tags!' do
let!(:target_tag) { create(:tag, category: :general) } let!(:target_tag) { create(:tag, category: :general) }
let!(:source_tag) { create(:tag, category: :general) } let!(:source_tag) { create(:tag, category: :general) }
@@ -226,14 +185,17 @@ RSpec.describe Tag, type: :model do
context 'when merging a simple source tag' do context 'when merging a simple source tag' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) } let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
it 'deletes the source tag, moves its post link, and keeps its name as an alias' do it 'deletes the source link, links the target, and aliases the discarded source tag' do
described_class.merge_tags!(target_tag, [source_tag]) described_class.merge_tags!(target_tag, [source_tag])
target_link = PostTag.find_by(post: post_record, tag: target_tag) target_link = PostTag.find_by(post: post_record, tag: target_tag)
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false) expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false) expect(source_tag.reload.post_count).to eq(0)
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id) expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(target_tag.reload.post_count).to eq(1) expect(target_tag.reload.post_count).to eq(1)
end end
@@ -256,76 +218,17 @@ RSpec.describe Tag, type: :model do
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false) expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_links).to contain_exactly(target_post_tag) expect(target_links).to contain_exactly(target_post_tag)
expect(source_tag.reload.post_count).to eq(0)
expect(PostTagSection.where(post: post_record, tag: source_tag)).to be_empty expect(PostTagSection.where(post: post_record, tag: source_tag)).to be_empty
expect(target_post_tag.reload.sections).to contain_exactly(target_section) expect(target_post_tag.reload.sections).to contain_exactly(target_section)
expect(Tag.unscoped.exists?(source_tag.id)).to be(false) expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id) expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(target_tag.reload.post_count).to eq(1) expect(target_tag.reload.post_count).to eq(1)
end end
end end
it 'keeps source history and records the new target alias after deleting the source' do
user = create_member_user!
source_name = source_tag.name
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name)
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
original_version = source_tag.tag_versions.first
described_class.merge_tags!(target_tag, [source_tag], created_by_user: user)
versions = TagVersion.where(tag_id: source_tag.id).order(:version_no)
expect(versions.pluck(:version_no, :event_type))
.to eq([[1, 'create'], [2, 'discard']])
expect(versions.first).to eq(original_version)
expect(versions.last).to have_attributes(
name: source_name, aliases: source_alias.name, created_by_user: user)
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
target_versions = target_tag.tag_versions.order(:version_no)
expect(target_versions.pluck(:event_type)).to eq(['create', 'update'])
expect(target_versions.last.aliases.split).to eq([source_name])
end
it 'deletes source relationships while preserving unrelated relationships' do
parent = create(:tag)
child = create(:tag)
nico_tag = create(:tag, :nico)
TagImplication.create!(tag: source_tag, parent_tag: parent)
TagImplication.create!(tag: child, parent_tag: source_tag)
kept_implication = TagImplication.create!(tag: target_tag, parent_tag: parent)
NicoTagRelation.create!(tag: source_tag, nico_tag:)
kept_relation = NicoTagRelation.create!(tag: target_tag, nico_tag:)
TagSimilarity.create!(tag: source_tag, target_tag:, cos: 0.5)
TagSimilarity.create!(tag: target_tag, target_tag: source_tag, cos: 0.5)
kept_similarity = TagSimilarity.create!(tag: target_tag, target_tag: parent,
cos: 0.5)
described_class.merge_tags!(target_tag, [source_tag])
expect(TagImplication.all).to contain_exactly(kept_implication)
expect(NicoTagRelation.all).to contain_exactly(kept_relation)
expect(TagSimilarity.all).to contain_exactly(kept_similarity)
expect(TagVersion.where(tag_id: source_tag.id).order(:version_no).last.parent_tag_ids)
.to eq(parent.id.to_s)
end
it 'preserves material history referencing the deleted source tag' do
source_tag.update!(category: :material)
target_tag.update!(category: :material)
material = Material.create!(tag: source_tag, url: 'https://example.com/material')
version = MaterialVersionRecorder.record!(
material:, event_type: :create, created_by_user: nil)
material.update!(tag: target_tag)
described_class.merge_tags!(target_tag, [source_tag])
expect(version.reload).to have_attributes(
tag_id: source_tag.id, tag_name: source_tag_name.name, tag_category: 'material')
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(material.reload.tag).to eq(target_tag)
end
context 'when source_tags includes the target itself' do context 'when source_tags includes the target itself' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) } let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
@@ -335,7 +238,7 @@ RSpec.describe Tag, type: :model do
target_link = PostTag.find_by(post: post_record, tag: target_tag) target_link = PostTag.find_by(post: post_record, tag: target_tag)
expect(Tag.find(target_tag.id)).to be_present expect(Tag.find(target_tag.id)).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false) expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false) expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present 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.canonical_id).to eq(target_tag.tag_name_id)
@@ -360,7 +263,7 @@ RSpec.describe Tag, type: :model do
) )
end end
it 'still merges and keeps the source name as an alias without validating it' do it 'still merges, but discards the source tag_name instead of aliasing it' do
described_class.merge_tags!(target_tag, [source_tag]) described_class.merge_tags!(target_tag, [source_tag])
target_link = PostTag.find_by(post: post_record, tag: target_tag) target_link = PostTag.find_by(post: post_record, tag: target_tag)
@@ -368,8 +271,7 @@ RSpec.describe Tag, type: :model do
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false) expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false) expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(target_tag.reload.post_count).to eq(1) expect(target_tag.reload.post_count).to eq(1)
end end
end end
@@ -386,23 +288,17 @@ RSpec.describe Tag, type: :model do
message: 'init') message: 'init')
end end
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do it 'rolls back the transaction' do
earlier_source = create(:tag)
earlier_name = earlier_source.tag_name
source_section = create(:post_tag_section, post: post_record, source_section = create(:post_tag_section, post: post_record,
tag: source_tag, tag: source_tag,
begin_ms: 1000, end_ms: 2000) begin_ms: 1000, end_ms: 2000)
expect { expect {
described_class.merge_tags!(target_tag, [earlier_source, source_tag]) described_class.merge_tags!(target_tag, [source_tag])
}.to raise_error(ActiveRecord::RecordInvalid) }.to raise_error(ActiveRecord::RecordInvalid)
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true) expect(Tag.with_discarded.find(source_tag.id)).not_to be_discarded
expect(earlier_name.reload.canonical_id).to be_nil expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
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)
expect(TagName.unscoped.exists?(source_tag_name.id)).to be(true)
expect(source_post_tag.reload.tag_id).to eq(source_tag.id) expect(source_post_tag.reload.tag_id).to eq(source_tag.id)
expect(source_post_tag.sections).to contain_exactly(source_section) expect(source_post_tag.sections).to contain_exactly(source_section)
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
@@ -413,45 +309,21 @@ RSpec.describe Tag, type: :model do
end end
context 'when merging a nico source tag' do context 'when merging a nico source tag' do
let!(:target_tag) do let!(:target_tag) { create(:tag, category: :nico, name: 'nico:foo') }
create(:tag, category: :nico, tag_name: create(:tag_name, name: 'nico:foo')) let!(:source_tag) { create(:tag, category: :nico, name: 'nico:bar') }
end
let!(:source_tag) do
create(:tag, category: :nico, tag_name: create(:tag_name, name: 'nico:bar'))
end
let!(:source_tag_name_id) { source_tag.tag_name_id } let!(:source_tag_name_id) { source_tag.tag_name_id }
it 'deletes the source tag and name instead of keeping an alias' do it 'discards the source tag_name instead of aliasing it' do
described_class.merge_tags!(target_tag, [source_tag]) described_class.merge_tags!(target_tag, [source_tag])
expect(Tag.unscoped.exists?(source_tag.id)).to be(false) discarded_source_tag = Tag.with_discarded.find(source_tag.id)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false) discarded_source_tag_name = TagName.with_discarded.find(source_tag_name_id)
expect(discarded_source_tag).to be_discarded
expect(discarded_source_tag_name).to be_discarded
expect(discarded_source_tag_name.canonical_id).to be_nil
expect(target_tag.reload.post_count).to eq(0) expect(target_tag.reload.post_count).to eq(0)
end end
it 'keeps nico history while deleting source links and allows recreating the name' do
linked_tag = create(:tag)
NicoTagRelation.create!(nico_tag: source_tag, tag: linked_tag)
kept_relation = NicoTagRelation.create!(nico_tag: target_tag, tag: linked_tag)
user = create_member_user!
source_name = source_tag.name
described_class.merge_tags!(target_tag, [source_tag], created_by_user: user)
expect(NicoTagRelation.all).to contain_exactly(kept_relation)
versions = NicoTagVersion.where(tag_id: source_tag.id).order(:version_no)
expect(versions.pluck(:version_no, :event_type))
.to eq([[1, 'create'], [2, 'discard']])
expect(versions.last).to have_attributes(
name: source_name, linked_tags: linked_tag.name, created_by_user: user)
recreated = described_class.find_or_create_by_tag_name!(source_name, category: :nico)
expect(recreated.id).not_to eq(source_tag.id)
expect(recreated.tag_name_id).not_to eq(source_tag_name_id)
expect(recreated.nico_tag_versions).to be_empty
expect(versions.reload.size).to eq(2)
end
end end
def snapshot_tags(post) def snapshot_tags(post)
+6 -2
ファイルの表示
@@ -1252,7 +1252,9 @@ RSpec.describe 'Posts API', type: :request do
context 'when nico tag already exists in tags' do context 'when nico tag already exists in tags' do
before do before do
Tag.find_or_create_by_tag_name!('nico:nico_tag', category: :nico) Tag.find_undiscard_or_create_by!(
tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
category: :nico)
end end
it 'returns 422 with tag field errors' do it 'returns 422 with tag field errors' do
@@ -1578,7 +1580,9 @@ RSpec.describe 'Posts API', type: :request do
context 'when nico tag already exists in tags' do context 'when nico tag already exists in tags' do
before do before do
Tag.find_or_create_by_tag_name!('nico:nico_tag', category: :nico) Tag.find_undiscard_or_create_by!(
tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
category: :nico)
end end
it 'returns 422 with tag field errors' do it 'returns 422 with tag field errors' do
+2 -1
ファイルの表示
@@ -8,7 +8,8 @@ RSpec.describe 'nico:sync' do
end end
def create_tag!(name, category:) def create_tag!(name, category:)
Tag.find_or_create_by_tag_name!(name, category:) tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) { |t| t.category = category }
end end
def link_nico_to_tag!(nico_tag, tag) def link_nico_to_tag!(nico_tag, tag)