コミットを比較
6
コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
f6d3dd1c10 | ||
|
|
785b5ef8bc | ||
|
|
b0df11c500 | ||
|
|
886128a7a8 | ||
|
|
6d1a8f0444 | ||
|
|
6f0442d633 |
@@ -236,10 +236,10 @@ class MaterialsController < ApplicationController
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_material_tag! tag_name_raw
|
def resolve_material_tag! locale, tag_name_raw
|
||||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
tag_name = TagName.find_or_create_by!(language_code: locale.language_code,
|
||||||
tag = tag_name.tag
|
name: tag_name_raw)
|
||||||
tag || Tag.create!(tag_name:, category: :material)
|
tag_name.tag || Tag.create!(tag_name:, category: :material)
|
||||||
end
|
end
|
||||||
|
|
||||||
def material_index_needs_tag_name? filters
|
def material_index_needs_tag_name? filters
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ 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]
|
||||||
@@ -329,7 +330,8 @@ 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(title:,
|
incoming_snapshot = post_incoming_snapshot(locale:,
|
||||||
|
title:,
|
||||||
original_created_from:,
|
original_created_from:,
|
||||||
original_created_before:,
|
original_created_before:,
|
||||||
tag_names:,
|
tag_names:,
|
||||||
@@ -358,7 +360,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
apply_post_snapshot!(post, snapshot_to_apply)
|
apply_post_snapshot!(locale, 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
|
||||||
@@ -685,11 +687,11 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
def post_incoming_snapshot locale:, 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!(tag_names, with_tagme: false, deny_deprecated: true,
|
Tag.normalise_tags!(locale, tag_names, with_tagme: false, deny_deprecated: true,
|
||||||
with_sections: true) =>
|
with_sections: true) =>
|
||||||
{ tags:, sections: }
|
{ tags:, sections: }
|
||||||
|
|
||||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||||
@@ -825,7 +827,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! post, snapshot
|
def apply_post_snapshot! locale, 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],
|
||||||
@@ -833,10 +835,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!(snapshot[:tag_names], with_tagme: false,
|
Tag.normalise_tags!(locale, snapshot[:tag_names],
|
||||||
deny_deprecated: true,
|
with_tagme: false,
|
||||||
with_sections: true) =>
|
deny_deprecated: true,
|
||||||
{ tags: editable_tags, sections: }
|
with_sections: true) => { 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
|
||||||
|
|||||||
@@ -573,7 +573,7 @@ class TagsController < ApplicationController
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
target_tag_name = TagName.with_discarded.find_by(name:)
|
target_tag_name = TagName.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?
|
||||||
|
|
||||||
@@ -585,17 +585,14 @@ 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.with_discarded.find_by(name:)
|
target_tag_name = TagName.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!(
|
promote_tag_alias!(tag, current_tag_name:, promoted_tag_name: target_tag_name)
|
||||||
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:
|
||||||
@@ -605,11 +602,9 @@ 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.with_discarded
|
TagName.where(canonical_id: current_tag_name.id)
|
||||||
.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)
|
||||||
@@ -640,7 +635,7 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
alias_names.each do |alias_name|
|
alias_names.each do |alias_name|
|
||||||
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
||||||
affected_tags << alias_tag_name.canonical&.tag
|
affected_tags << alias_tag_name.canonical&.tag
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -655,7 +650,7 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
alias_names.each do |alias_name|
|
alias_names.each do |alias_name|
|
||||||
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
||||||
alias_tag_name.update!(canonical: tag.tag_name)
|
alias_tag_name.update!(canonical: tag.tag_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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_undiscard_or_create_by!(name: title)
|
tag_name = TagName.find_or_create_by!(name: title)
|
||||||
|
|
||||||
page =
|
page =
|
||||||
Wiki::Commit.create_content!(
|
Wiki::Commit.create_content!(
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class Language < ApplicationRecord
|
||||||
|
has_many :languages, class_name: 'Locale', foreign_key: :language_code
|
||||||
|
end
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
@@ -63,16 +63,19 @@ 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,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class Script < ApplicationRecord
|
||||||
|
;
|
||||||
|
end
|
||||||
+33
-22
@@ -2,8 +2,6 @@ require 'set'
|
|||||||
|
|
||||||
|
|
||||||
class Tag < ApplicationRecord
|
class Tag < ApplicationRecord
|
||||||
include MyDiscard
|
|
||||||
|
|
||||||
class NicoTagNormalisationError < ArgumentError
|
class NicoTagNormalisationError < ArgumentError
|
||||||
;
|
;
|
||||||
end
|
end
|
||||||
@@ -54,6 +52,7 @@ 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
|
||||||
|
|
||||||
@@ -103,18 +102,20 @@ 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!('タグ希望', category: :meta)
|
def self.tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta)
|
||||||
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
|
def self.bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
|
||||||
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
|
def self.no_deerjikist =
|
||||||
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
|
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
|
||||||
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
|
def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
|
||||||
def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
|
def self.niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
|
||||||
|
def self.youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
|
||||||
|
|
||||||
def self.normalise_tags! tag_names, with_tagme: true,
|
def self.normalise_tags! locale, tag_names,
|
||||||
with_no_deerjikist: true,
|
with_tagme: true,
|
||||||
deny_nico: true,
|
with_no_deerjikist: true,
|
||||||
deny_deprecated: false,
|
deny_nico: true,
|
||||||
with_sections: false
|
deny_deprecated: 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
|
||||||
@@ -143,7 +144,7 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
name = TagName.canonicalise(name).first
|
name = TagName.canonicalise(name).first
|
||||||
|
|
||||||
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
|
find_or_create_by_tag_name!(locale, 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
|
||||||
@@ -231,13 +232,23 @@ 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! name, category:
|
def self.find_or_create_by_tag_name! locale, name, category:
|
||||||
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
language_code = locale.language_code
|
||||||
tn = tn.canonical if tn.canonical_id?
|
name = name.to_s.strip
|
||||||
|
|
||||||
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
|
tn = TagName.find_or_create_by!(language_code:, name:) do
|
||||||
t.category = category
|
_1.script_code = locale.script_code
|
||||||
|
_1.primary_flg = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tag = tn.tag
|
||||||
|
unless tag
|
||||||
|
tag = Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do
|
||||||
|
_1.category = category
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tag
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
@@ -273,10 +284,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.discard!
|
source_tag.destroy!
|
||||||
|
|
||||||
if source_tag.nico?
|
if source_tag.nico?
|
||||||
source_tag_name.discard!
|
source_tag_name.destroy!
|
||||||
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)
|
||||||
@@ -297,7 +308,7 @@ class Tag < ApplicationRecord
|
|||||||
target_tag.reload
|
target_tag.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
def snapshot_aliases = tag_name.aliases.kept.order(:name).pluck(:name)
|
def snapshot_aliases = tag_name.aliases.order(:name).pluck(:name)
|
||||||
|
|
||||||
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
class TagName < ApplicationRecord
|
class TagName < ApplicationRecord
|
||||||
include MyDiscard
|
belongs_to :tag
|
||||||
|
|
||||||
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
|
||||||
@@ -23,6 +21,12 @@ 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
|
||||||
|
|||||||
@@ -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.discard!
|
tn.destroy!
|
||||||
|
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
生成ファイル
+13
-28
@@ -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_010000) do
|
ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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
|
||||||
@@ -72,7 +72,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
t.index ["correct_post_id"], name: "index_gekanator_games_on_correct_post_id"
|
t.index ["correct_post_id"], name: "index_gekanator_games_on_correct_post_id"
|
||||||
t.index ["guessed_post_id"], name: "index_gekanator_games_on_guessed_post_id"
|
t.index ["guessed_post_id"], name: "index_gekanator_games_on_guessed_post_id"
|
||||||
t.index ["user_id"], name: "index_gekanator_games_on_user_id"
|
t.index ["user_id"], name: "index_gekanator_games_on_user_id"
|
||||||
t.check_constraint "`question_count` >= 0", name: "chk_gekanator_games_question_count_nonnegative"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "gekanator_question_examples", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "gekanator_question_examples", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -275,10 +274,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.bigint "created_by_user_id"
|
t.bigint "created_by_user_id"
|
||||||
t.index ["created_at"], name: "index_nico_tag_versions_on_created_at"
|
t.index ["created_at"], name: "index_nico_tag_versions_on_created_at"
|
||||||
t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
|
t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at"
|
||||||
t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
|
t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at"
|
||||||
t.index ["tag_id", "version_no"], name: "index_nico_tag_versions_on_tag_id_and_version_no", unique: true
|
t.index ["tag_id", "version_no"], name: "index_nico_tag_versions_on_tag_id_and_version_no", unique: true
|
||||||
t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -287,14 +285,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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.index ["parent_post_id"], name: "index_post_implications_on_parent_post_id"
|
t.index ["parent_post_id"], name: "index_post_implications_on_parent_post_id"
|
||||||
t.check_constraint "`post_id` <> `parent_post_id`", name: "chk_post_implications_no_self"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "post_similarities", primary_key: ["post_id", "target_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "post_similarities", primary_key: ["post_id", "target_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "post_id", null: false
|
t.bigint "post_id", null: false
|
||||||
t.bigint "target_post_id", null: false
|
t.bigint "target_post_id", null: false
|
||||||
t.float "cos", null: false
|
t.float "cos", null: false
|
||||||
t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos", order: { cos: :desc }
|
t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos"
|
||||||
t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id"
|
t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -350,8 +347,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
t.index ["post_id"], name: "index_post_versions_on_post_id"
|
t.index ["post_id"], name: "index_post_versions_on_post_id"
|
||||||
t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id"
|
t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id"
|
||||||
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive"
|
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive"
|
||||||
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "post_versions_event_type_valid"
|
|
||||||
t.check_constraint "`version_no` > 0", name: "post_versions_version_no_positive"
|
|
||||||
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\",\"nico\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"id\",\"version_no\",\"name\",\"category\",\"sections\"],\"additionalProperties\":false}}',`tags_json`)", name: "chk_post_versions_tags_json_schema"
|
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\",\"nico\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"id\",\"version_no\",\"name\",\"category\",\"sections\"],\"additionalProperties\":false}}',`tags_json`)", name: "chk_post_versions_tags_json_schema"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -370,7 +365,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
t.index ["url"], name: "index_posts_on_url", unique: true
|
t.index ["url"], name: "index_posts_on_url", unique: true
|
||||||
t.index ["video_ms", "id"], name: "idx_posts_video_ms_id"
|
t.index ["video_ms", "id"], name: "idx_posts_video_ms_id"
|
||||||
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_posts_video_ms_positive"
|
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_posts_video_ms_positive"
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -378,6 +372,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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.string "theme", default: "system", null: false
|
t.string "theme", default: "system", null: false
|
||||||
|
t.string "display_density", default: "comfortable", null: false
|
||||||
|
t.string "font_size", default: "normal", null: false
|
||||||
|
t.integer "post_list_limit", default: 50, null: false
|
||||||
|
t.string "post_list_order", default: "created_at_desc", null: false
|
||||||
|
t.string "viewed_post_display", default: "show", null: false
|
||||||
|
t.boolean "tag_autocomplete_nico", default: true, null: false
|
||||||
t.string "auto_fetch_title", default: "manual", null: false
|
t.string "auto_fetch_title", default: "manual", null: false
|
||||||
t.string "auto_fetch_thumbnail", default: "manual", null: false
|
t.string "auto_fetch_thumbnail", default: "manual", null: false
|
||||||
t.string "wiki_editor_mode", default: "split", null: false
|
t.string "wiki_editor_mode", default: "split", null: false
|
||||||
@@ -409,9 +409,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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
|
||||||
|
|
||||||
@@ -419,7 +417,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "target_tag_id", null: false
|
t.bigint "target_tag_id", null: false
|
||||||
t.float "cos", null: false
|
t.float "cos", null: false
|
||||||
t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos", order: { cos: :desc }
|
t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos"
|
||||||
t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id"
|
t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -435,10 +433,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.bigint "created_by_user_id"
|
t.bigint "created_by_user_id"
|
||||||
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
||||||
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
|
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at"
|
||||||
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
|
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at"
|
||||||
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
|
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
|
||||||
t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -448,13 +445,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -621,13 +615,11 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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.datetime "discarded_at"
|
t.datetime "discarded_at"
|
||||||
t.integer "next_asset_no", default: 1, null: false
|
|
||||||
t.integer "version_no", null: false
|
t.integer "version_no", null: false
|
||||||
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
||||||
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
||||||
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
|
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
|
||||||
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_wiki_pages_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -672,8 +664,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
t.index ["created_by_user_id"], name: "index_wiki_versions_on_created_by_user_id"
|
t.index ["created_by_user_id"], name: "index_wiki_versions_on_created_by_user_id"
|
||||||
t.index ["wiki_page_id", "version_no"], name: "index_wiki_versions_on_wiki_page_id_and_version_no", unique: true
|
t.index ["wiki_page_id", "version_no"], name: "index_wiki_versions_on_wiki_page_id_and_version_no", unique: true
|
||||||
t.index ["wiki_page_id"], name: "index_wiki_versions_on_wiki_page_id"
|
t.index ["wiki_page_id"], name: "index_wiki_versions_on_wiki_page_id"
|
||||||
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "wiki_versions_event_type_valid"
|
|
||||||
t.check_constraint "`version_no` > 0", name: "wiki_versions_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
@@ -698,7 +688,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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"
|
||||||
@@ -707,7 +696,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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"
|
||||||
@@ -726,7 +714,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) 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"
|
||||||
@@ -753,8 +740,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_010000) do
|
|||||||
add_foreign_key "user_post_views", "posts"
|
add_foreign_key "user_post_views", "posts"
|
||||||
add_foreign_key "user_post_views", "users"
|
add_foreign_key "user_post_views", "users"
|
||||||
add_foreign_key "user_theme_slots", "users"
|
add_foreign_key "user_theme_slots", "users"
|
||||||
add_foreign_key "wiki_assets", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "wiki_assets", "wiki_pages"
|
|
||||||
add_foreign_key "wiki_pages", "tag_names"
|
add_foreign_key "wiki_pages", "tag_names"
|
||||||
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
||||||
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
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
|
||||||
@@ -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.exists?(source.id)).to be(false)
|
expect(TagName.unscoped.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,7 +75,27 @@ 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.exists?(source_tag_name_id)).to be(false)
|
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the sanitised name is an alias of an existing tag' do
|
||||||
|
let!(:existing_tag) { create(:tag) }
|
||||||
|
let!(: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
|
||||||
|
|
||||||
@@ -92,13 +112,15 @@ 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
|
||||||
expect(TagName.find_by(name: 'foobar')&.tag&.id).to eq(existing_tag.id)
|
post = create(:post)
|
||||||
expect(TagName.find_by(name: 'foo_bar')&.tag&.id).to eq(source_tag.id)
|
PostTag.create!(post:, tag: source_tag)
|
||||||
|
|
||||||
described_class.apply!
|
described_class.apply!
|
||||||
|
|
||||||
expect(Tag.exists?(source_tag.id)).to be(false)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
expect(TagName.exists?(source_tag.tag_name_id)).to be(false)
|
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||||
|
expect(post.reload.tags).to contain_exactly(existing_tag)
|
||||||
|
expect(existing_tag.reload.name).to eq('foobar')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+152
-24
@@ -173,6 +173,47 @@ 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) }
|
||||||
@@ -185,17 +226,14 @@ 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 link, links the target, and aliases the discarded source tag' do
|
it 'deletes the source tag, moves its post link, and keeps its name as an alias' 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(source_tag.reload.post_count).to eq(0)
|
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
|
||||||
@@ -218,17 +256,76 @@ 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.with_discarded.find(source_tag.id)).to be_discarded
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
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) }
|
||||||
|
|
||||||
@@ -238,7 +335,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.with_discarded.find(source_tag.id)).to be_discarded
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
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)
|
||||||
@@ -263,7 +360,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'still merges, but discards the source tag_name instead of aliasing it' do
|
it 'still merges and keeps the source name as an alias without validating 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)
|
||||||
@@ -271,7 +368,8 @@ 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.with_discarded.find(source_tag.id)).to be_discarded
|
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(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -288,17 +386,23 @@ RSpec.describe Tag, type: :model do
|
|||||||
message: 'init')
|
message: 'init')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rolls back the transaction' do
|
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do
|
||||||
|
earlier_source = create(:tag)
|
||||||
|
earlier_name = earlier_source.tag_name
|
||||||
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, [source_tag])
|
described_class.merge_tags!(target_tag, [earlier_source, source_tag])
|
||||||
}.to raise_error(ActiveRecord::RecordInvalid)
|
}.to raise_error(ActiveRecord::RecordInvalid)
|
||||||
|
|
||||||
expect(Tag.with_discarded.find(source_tag.id)).not_to be_discarded
|
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
|
||||||
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
|
expect(earlier_name.reload.canonical_id).to be_nil
|
||||||
|
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
|
||||||
@@ -309,21 +413,45 @@ 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) { create(:tag, category: :nico, name: 'nico:foo') }
|
let!(:target_tag) do
|
||||||
let!(:source_tag) { create(:tag, category: :nico, name: 'nico:bar') }
|
create(:tag, category: :nico, tag_name: create(:tag_name, name: 'nico:foo'))
|
||||||
|
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 'discards the source tag_name instead of aliasing it' do
|
it 'deletes the source tag and name instead of keeping an alias' do
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
described_class.merge_tags!(target_tag, [source_tag])
|
||||||
|
|
||||||
discarded_source_tag = Tag.with_discarded.find(source_tag.id)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
discarded_source_tag_name = TagName.with_discarded.find(source_tag_name_id)
|
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -1252,9 +1252,7 @@ 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_undiscard_or_create_by!(
|
Tag.find_or_create_by_tag_name!('nico:nico_tag', category: :nico)
|
||||||
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
|
||||||
@@ -1580,9 +1578,7 @@ 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_undiscard_or_create_by!(
|
Tag.find_or_create_by_tag_name!('nico:nico_tag', category: :nico)
|
||||||
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
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ RSpec.describe 'nico:sync' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_tag!(name, category:)
|
def create_tag!(name, category:)
|
||||||
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
Tag.find_or_create_by_tag_name!(name, category:)
|
||||||
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)
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする