コミットを比較
8
コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
694ed60431 | ||
|
|
493ca749b4 | ||
|
|
b217a307fe | ||
|
|
4b99743435 | ||
|
|
4fd95b74f1 | ||
|
|
3a5d79b5d7 | ||
|
|
6e33c5f192 | ||
|
|
2425f1af1f |
@@ -237,7 +237,7 @@ class MaterialsController < ApplicationController
|
||||
end
|
||||
|
||||
def resolve_material_tag! tag_name_raw
|
||||
tag_name = TagName.find_or_create_by!(name: tag_name_raw)
|
||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||
tag = tag_name.tag
|
||||
tag || Tag.create!(tag_name:, category: :material)
|
||||
end
|
||||
|
||||
@@ -15,22 +15,23 @@ class NicoTagsController < ApplicationController
|
||||
limit = 1 if limit < 1
|
||||
|
||||
post_tag_max_sql =
|
||||
PostExternalTag
|
||||
.select('external_tag_id, MAX(created_at) AS max_created_at')
|
||||
.group('external_tag_id')
|
||||
PostTag
|
||||
.select('tag_id, MAX(created_at) AS max_created_at')
|
||||
.group('tag_id')
|
||||
.to_sql
|
||||
|
||||
q =
|
||||
ExternalTag
|
||||
.joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \
|
||||
'ON post_tag_max.external_tag_id = external_tags.id')
|
||||
q = q.where('external_tags.name LIKE ?', "%#{ name }%") if name
|
||||
q = Tag.nico_tags
|
||||
.joins(:tag_name)
|
||||
.joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \
|
||||
'ON post_tag_max.tag_id = tags.id')
|
||||
.includes(:tag_name, tag_name: :wiki_page, linked_tags: { tag_name: :wiki_page })
|
||||
q = q.where('tag_names.name LIKE ?', "%#{ name }%") if name
|
||||
if linked_tag
|
||||
linked_tag_ids =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.where('tag_names.name LIKE ?', "%#{ linked_tag }%")
|
||||
.pluck(:id)
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.where('tag_names.name LIKE ?', "%#{ linked_tag }%")
|
||||
.pluck(:id)
|
||||
linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id)
|
||||
q = q.where(id: linked_nico_tag_ids)
|
||||
end
|
||||
@@ -71,7 +72,8 @@ class NicoTagsController < ApplicationController
|
||||
|
||||
id = params[:id].to_i
|
||||
|
||||
tag = ExternalTag.find(id)
|
||||
tag = Tag.find(id)
|
||||
return render_bad_request('ニコニコ・タグを指定してください.') unless tag.nico?
|
||||
|
||||
linked_tag_names = params[:tags].to_s.split
|
||||
linked_tags = nil
|
||||
@@ -79,6 +81,9 @@ class NicoTagsController < ApplicationController
|
||||
ApplicationRecord.transaction do
|
||||
linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
if linked_tags.any? { |t| t.nico? }
|
||||
raise Tag::NicoTagNormalisationError
|
||||
end
|
||||
|
||||
TagVersioning.record_tag_snapshots!(linked_tags, created_by_user: current_user)
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ class PostVersionsController < ApplicationController
|
||||
'prev.original_created_before AS prev_original_created_before')
|
||||
q = q.where('post_versions.post_id = ?', post_id) if post_id
|
||||
if tag_id
|
||||
q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('tag_id', #{ tag_id })) " +
|
||||
"OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('tag_id', #{ tag_id }))")
|
||||
q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('id', #{ tag_id })) " +
|
||||
"OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('id', #{ tag_id }))")
|
||||
end
|
||||
|
||||
count = q.except(:select, :order, :limit, :offset).count
|
||||
@@ -113,18 +113,21 @@ class PostVersionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def build_version_tags cur_tags, prev_tags
|
||||
def build_version_tags(cur_tags, prev_tags)
|
||||
(cur_tags | prev_tags).map do |name|
|
||||
type =
|
||||
if cur_tags.include?(name) && prev_tags.include?(name)
|
||||
'context'
|
||||
elsif cur_tags.include?(name)
|
||||
'added'
|
||||
else
|
||||
'removed'
|
||||
end
|
||||
if cur_tags.include?(name) && prev_tags.include?(name)
|
||||
'context'
|
||||
elsif cur_tags.include?(name)
|
||||
'added'
|
||||
else
|
||||
'removed'
|
||||
end
|
||||
|
||||
{ name:, type: }
|
||||
{
|
||||
name:,
|
||||
type:
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -671,6 +671,7 @@ class PostsController < ApplicationController
|
||||
post
|
||||
.post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.merge(Tag.not_nico)
|
||||
.merge(Tag.where(deprecated_at: nil))
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.order('tag_names.name')
|
||||
@@ -688,7 +689,8 @@ class PostsController < ApplicationController
|
||||
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
||||
validate_original_created_values!(original_created_from, original_created_before)
|
||||
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||
with_sections: true) => { tags:, sections: }
|
||||
with_sections: true) =>
|
||||
{ tags:, sections: }
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:)
|
||||
@@ -831,12 +833,15 @@ class PostsController < ApplicationController
|
||||
original_created_from: snapshot[:original_created_from],
|
||||
original_created_before: snapshot[:original_created_before])
|
||||
|
||||
Tag.normalise_tags!(snapshot[:tag_names],
|
||||
with_tagme: false,
|
||||
deny_deprecated: true,
|
||||
with_sections: true) => { tags:, sections: }
|
||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user)
|
||||
Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false,
|
||||
deny_deprecated: true,
|
||||
with_sections: true) =>
|
||||
{ tags: editable_tags, sections: }
|
||||
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
|
||||
|
||||
readonly_tags = post.tags.nico.to_a
|
||||
|
||||
tags = readonly_tags + editable_tags
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
|
||||
post.video_ms = tags.any? { _1.id == Tag.video.id } ? snapshot[:video_ms] : nil
|
||||
|
||||
@@ -10,6 +10,7 @@ class TagChildrenController < ApplicationController
|
||||
|
||||
parent = Tag.find(parent_id)
|
||||
child = Tag.find(child_id)
|
||||
return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico?
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
TagVersioning.ensure_snapshot!(child, created_by_user: current_user)
|
||||
@@ -32,6 +33,7 @@ class TagChildrenController < ApplicationController
|
||||
|
||||
parent = Tag.find(parent_id)
|
||||
child = Tag.find(child_id)
|
||||
return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico?
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
TagVersioning.ensure_snapshot!(child, created_by_user: current_user)
|
||||
|
||||
@@ -100,14 +100,13 @@ class TagsController < ApplicationController
|
||||
|
||||
def autocomplete
|
||||
q = params[:q].to_s.strip.sub(/\Anot:/i, '')
|
||||
prefix = "#{ ActiveRecord::Base.sanitize_sql_like(q) }%"
|
||||
|
||||
with_nico = bool?(:nico, default: true)
|
||||
present_only = bool?(:present, default: true)
|
||||
|
||||
alias_rows =
|
||||
TagName
|
||||
.where('name LIKE ?', prefix)
|
||||
.where('name LIKE ?', "#{ q }%")
|
||||
.where.not(canonical_id: nil)
|
||||
.pluck(:canonical_id, :name)
|
||||
|
||||
@@ -119,57 +118,29 @@ class TagsController < ApplicationController
|
||||
matched_alias_by_tag_name_id[canonical_id] ||= alias_name
|
||||
end
|
||||
|
||||
base =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.where(deprecated_at: nil)
|
||||
base = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.where(deprecated_at: nil)
|
||||
base = base.where('tags.post_count > 0') if present_only
|
||||
|
||||
canonical_hit = base.where('tag_names.name LIKE ?', prefix)
|
||||
canonical_hit =
|
||||
base
|
||||
.where(((with_nico ? '(tags.category = ? AND tag_names.name LIKE ?) OR ' : '') +
|
||||
'tag_names.name LIKE ?'),
|
||||
*(with_nico ? ['nico', "nico:#{ q }%"] : []), "#{ q }%")
|
||||
|
||||
internal_tags =
|
||||
if with_nico
|
||||
tags =
|
||||
if canonical_ids.present?
|
||||
canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq))
|
||||
else
|
||||
canonical_hit
|
||||
end
|
||||
|
||||
internal_rows =
|
||||
internal_tags
|
||||
.order(Arel.sql('tags.post_count DESC, tag_names.name'))
|
||||
.limit(20)
|
||||
.map { |tag|
|
||||
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
|
||||
}
|
||||
tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a
|
||||
|
||||
return render json: internal_rows unless with_nico
|
||||
|
||||
external_base = ExternalTag.all
|
||||
external_base = external_base.where('post_count > 0') if present_only
|
||||
|
||||
external_rows =
|
||||
external_base
|
||||
.where("CONCAT(platform, ':', name) LIKE ? OR name LIKE ?", prefix, prefix)
|
||||
.order(post_count: :desc, name: :asc)
|
||||
.limit(20)
|
||||
.map { |tag|
|
||||
{ 'id' => tag.id,
|
||||
'name' => "#{ tag.platform }:#{ tag.name }",
|
||||
'category' => 'nico',
|
||||
'deprecated_at' => nil,
|
||||
'created_at' => tag.created_at,
|
||||
'updated_at' => tag.created_at,
|
||||
'post_count' => tag.post_count,
|
||||
'matched_alias' => nil }
|
||||
}
|
||||
|
||||
rows =
|
||||
(internal_rows + external_rows)
|
||||
.sort_by { |row| [-row['post_count'], row['name']] }
|
||||
.first(20)
|
||||
|
||||
render json: rows
|
||||
render json: tags.map { |tag|
|
||||
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
|
||||
}
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -311,7 +282,11 @@ class TagsController < ApplicationController
|
||||
parent_names = params[:parent_tags].to_s.split.uniq
|
||||
deprecated = bool?(:deprecated)
|
||||
|
||||
if category == 'nico'
|
||||
if tag.nico? && deprecated
|
||||
return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated
|
||||
end
|
||||
|
||||
if tag.nico? || category == 'nico'
|
||||
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
||||
end
|
||||
|
||||
@@ -359,9 +334,13 @@ class TagsController < ApplicationController
|
||||
|
||||
tag = Tag.find(params[:id])
|
||||
|
||||
if tag.nico? && deprecated_given && deprecated
|
||||
return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated
|
||||
end
|
||||
|
||||
return unless validate_tag_rename(tag, name)
|
||||
|
||||
if category.present? && category == 'nico'
|
||||
if tag.nico? || (category.present? && category == 'nico')
|
||||
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
||||
end
|
||||
|
||||
@@ -568,6 +547,11 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil
|
||||
if tag.nico?
|
||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
return
|
||||
end
|
||||
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
|
||||
return unless name_changed
|
||||
@@ -589,7 +573,7 @@ class TagsController < ApplicationController
|
||||
return false
|
||||
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.canonical_id?
|
||||
|
||||
@@ -601,14 +585,17 @@ class TagsController < ApplicationController
|
||||
return if name == 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?
|
||||
current_tag_name.update!(name:)
|
||||
return
|
||||
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
|
||||
|
||||
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
|
||||
@@ -618,9 +605,11 @@ class TagsController < ApplicationController
|
||||
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
|
||||
end
|
||||
|
||||
promoted_tag_name.undiscard! if promoted_tag_name.discarded?
|
||||
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)
|
||||
.find_each do |alias_tag_name|
|
||||
alias_tag_name.update!(canonical: promoted_tag_name)
|
||||
@@ -651,7 +640,7 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -666,7 +655,7 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -677,7 +666,8 @@ class TagsController < ApplicationController
|
||||
|
||||
def update_parent_tags! tag, parent_names
|
||||
parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
with_no_deerjikist: false,
|
||||
deny_nico: true)
|
||||
|
||||
old_parent_tags = tag.parents.to_a
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ class WikiPagesController < ApplicationController
|
||||
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.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 =
|
||||
Wiki::Commit.create_content!(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
class ExternalTag < ApplicationRecord
|
||||
enum :platform, nico: 'nico'
|
||||
validates :platform, presence: true, inclusion: { in: ExternalTag.platforms.keys }
|
||||
|
||||
has_many :post_external_tags, dependent: :delete_all
|
||||
has_many :posts, through: :post_external_tags
|
||||
|
||||
has_many :nico_tag_versions, foreign_key: :tag_id, inverse_of: :external_tag
|
||||
|
||||
has_many :nico_tag_relations,
|
||||
foreign_key: :nico_tag_id,
|
||||
inverse_of: :nico_tag,
|
||||
dependent: :destroy
|
||||
|
||||
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
||||
|
||||
def snapshot_linked_tag_names
|
||||
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,6 @@
|
||||
class NicoTagRelation < ApplicationRecord
|
||||
belongs_to :nico_tag,
|
||||
class_name: 'ExternalTag',
|
||||
foreign_key: :nico_tag_id,
|
||||
inverse_of: :nico_tag_relations
|
||||
belongs_to :tag, class_name: 'Tag', foreign_key: :tag_id
|
||||
belongs_to :nico_tag, class_name: 'Tag'
|
||||
belongs_to :tag, class_name: 'Tag'
|
||||
|
||||
validates :nico_tag_id, presence: true
|
||||
validates :tag_id, presence: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class NicoTagVersion < ApplicationRecord
|
||||
include VersionRecord
|
||||
|
||||
belongs_to :external_tag, foreign_key: :tag_id, inverse_of: :nico_tag_versions
|
||||
belongs_to :tag
|
||||
|
||||
validates :name, presence: true
|
||||
end
|
||||
|
||||
+13
-27
@@ -63,19 +63,16 @@ class Post < ApplicationRecord
|
||||
has_many :user_post_views, dependent: :delete_all
|
||||
has_many :post_similarities, dependent: :delete_all
|
||||
has_many :post_versions
|
||||
|
||||
has_many :gekanator_guessed_games,
|
||||
class_name: 'GekanatorGame',
|
||||
foreign_key: :guessed_post_id,
|
||||
dependent: :delete_all,
|
||||
inverse_of: :guessed_post
|
||||
|
||||
has_many :gekanator_correct_games,
|
||||
class_name: 'GekanatorGame',
|
||||
foreign_key: :correct_post_id,
|
||||
dependent: :delete_all,
|
||||
inverse_of: :correct_post
|
||||
|
||||
has_many :gekanator_question_examples, dependent: :delete_all
|
||||
|
||||
has_many :parent_post_implications,
|
||||
@@ -92,9 +89,6 @@ class Post < ApplicationRecord
|
||||
inverse_of: :parent_post
|
||||
has_many :children, through: :child_post_implications, source: :post
|
||||
|
||||
has_many :post_external_tags, dependent: :delete_all
|
||||
has_many :external_tags, through: :post_external_tags
|
||||
|
||||
has_one_attached :thumbnail
|
||||
|
||||
attribute :version_no, :integer, default: 1
|
||||
@@ -153,27 +147,19 @@ class Post < ApplicationRecord
|
||||
end
|
||||
|
||||
def snapshot_tags_json
|
||||
tag_snapshots =
|
||||
post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.order('tags.id')
|
||||
.map { |pt|
|
||||
{ 'tag_id' => pt.tag.id,
|
||||
'version_no' => pt.tag.version_no,
|
||||
'name' => pt.tag.name,
|
||||
'category' => pt.tag.category,
|
||||
'sections' => pt.sections.sort_by(&:begin_ms).map {
|
||||
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
|
||||
} }
|
||||
}
|
||||
|
||||
external_tag_snapshots =
|
||||
post_external_tags.order(:external_tag_id).map {
|
||||
{ 'external_tag_id' => _1.external_tag_id }
|
||||
}
|
||||
|
||||
tag_snapshots + external_tag_snapshots
|
||||
post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.order('tags.id')
|
||||
.map do |pt|
|
||||
{ 'id' => pt.tag.id,
|
||||
'version_no' => pt.tag.version_no,
|
||||
'name' => pt.tag.name,
|
||||
'category' => pt.tag.category,
|
||||
'sections' => pt.sections.sort_by(&:begin_ms).map {
|
||||
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
|
||||
} }
|
||||
end
|
||||
end
|
||||
|
||||
def self.section_literal section
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
class PostExternalTag < ApplicationRecord
|
||||
belongs_to :post
|
||||
belongs_to :external_tag
|
||||
end
|
||||
+34
-15
@@ -2,6 +2,8 @@ require 'set'
|
||||
|
||||
|
||||
class Tag < ApplicationRecord
|
||||
include MyDiscard
|
||||
|
||||
class NicoTagNormalisationError < ArgumentError
|
||||
;
|
||||
end
|
||||
@@ -28,10 +30,11 @@ class Tag < ApplicationRecord
|
||||
has_many :post_tags, inverse_of: :tag
|
||||
has_many :posts, through: :post_tags
|
||||
|
||||
has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy
|
||||
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
||||
|
||||
has_many :reversed_nico_tag_relations,
|
||||
class_name: 'NicoTagRelation',
|
||||
foreign_key: :nico_tag_id,
|
||||
dependent: :destroy
|
||||
class_name: 'NicoTagRelation', foreign_key: :tag_id, dependent: :destroy
|
||||
has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag
|
||||
|
||||
has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy
|
||||
@@ -49,6 +52,7 @@ class Tag < ApplicationRecord
|
||||
has_many :materials
|
||||
|
||||
has_many :tag_versions
|
||||
has_many :nico_tag_versions
|
||||
|
||||
belongs_to :tag_name
|
||||
delegate :wiki_page, to: :tag_name
|
||||
@@ -63,15 +67,17 @@ class Tag < ApplicationRecord
|
||||
character: 'character',
|
||||
general: 'general',
|
||||
material: 'material',
|
||||
nico: 'nico',
|
||||
meta: 'meta'
|
||||
|
||||
validates :category, presence: true, inclusion: { in: Tag.categories.keys }
|
||||
|
||||
validate :tag_name_mustnt_start_with_nico
|
||||
validate :nico_tag_name_must_start_with_nico
|
||||
validate :tag_name_must_be_canonical
|
||||
validate :category_must_be_deerjikist_with_deerjikists
|
||||
validate :nico_tags_cannot_be_deprecated
|
||||
|
||||
def self.nico_tags = ExternalTag.where(platform: :nico)
|
||||
scope :nico_tags, -> { nico }
|
||||
|
||||
CATEGORY_PREFIXES = {
|
||||
'general:' => :general,
|
||||
@@ -106,9 +112,10 @@ class Tag < ApplicationRecord
|
||||
|
||||
def self.normalise_tags! tag_names, with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_nico: true,
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
end
|
||||
|
||||
@@ -225,10 +232,10 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.find_or_create_by_tag_name! name, category:
|
||||
tn = TagName.find_or_create_by!(name: name.to_s.strip)
|
||||
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
||||
tn = tn.canonical if tn.canonical_id?
|
||||
|
||||
Tag.find_or_create_by!(tag_name_id: tn.id) do |t|
|
||||
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
|
||||
t.category = category
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
@@ -266,10 +273,14 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
|
||||
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
|
||||
source_tag.destroy!
|
||||
source_tag.discard!
|
||||
|
||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||
updated_at: Time.current)
|
||||
if source_tag.nico?
|
||||
source_tag_name.discard!
|
||||
else
|
||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||
updated_at: Time.current)
|
||||
end
|
||||
|
||||
TagVersioning.record!(target_tag, event_type: :update, created_by_user:)
|
||||
end
|
||||
@@ -286,7 +297,7 @@ class Tag < ApplicationRecord
|
||||
target_tag.reload
|
||||
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)
|
||||
|
||||
@@ -296,9 +307,11 @@ class Tag < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def tag_name_mustnt_start_with_nico
|
||||
if name.to_s.downcase.start_with?('nico:')
|
||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||
def nico_tag_name_must_start_with_nico
|
||||
n = name.to_s
|
||||
if ((nico? && !(n.downcase.start_with?('nico:'))) ||
|
||||
(!(nico?) && n.downcase.start_with?('nico:')))
|
||||
errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -339,4 +352,10 @@ class Tag < ApplicationRecord
|
||||
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
def nico_tags_cannot_be_deprecated
|
||||
if nico? && deprecated_at.present?
|
||||
errors.add :deprecated_at, 'ニコタグは廃止できません.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class TagName < ApplicationRecord
|
||||
include MyDiscard
|
||||
|
||||
has_one :tag
|
||||
has_one :wiki_page
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class TagNameSanitisationRule < ApplicationRecord
|
||||
elsif source_tag
|
||||
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
|
||||
end
|
||||
tn.destroy!
|
||||
tn.discard!
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
class NicoTagVersionRecorder < VersionRecorder
|
||||
def self.record! external_tag:, event_type:, created_by_user:
|
||||
new(external_tag:, event_type:, created_by_user:).record!
|
||||
def self.record! tag:, event_type:, created_by_user:
|
||||
new(tag:, event_type:, created_by_user:).record!
|
||||
end
|
||||
|
||||
def initialize external_tag:, event_type:, created_by_user:
|
||||
super(record: external_tag, event_type:, created_by_user:)
|
||||
def initialize tag:, event_type:, created_by_user:
|
||||
super(record: tag, event_type:, created_by_user:)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def version_class = NicoTagVersion
|
||||
def version_association = :nico_tag_versions
|
||||
def record_key = :external_tag
|
||||
def record_key = :tag
|
||||
|
||||
def snapshot_attributes
|
||||
{ name: "#{ @record.platform }:#{ @record.name }",
|
||||
linked_tags: @record.snapshot_linked_tag_names.join(' ') }
|
||||
{ name: @record.name, linked_tags: @record.snapshot_linked_tag_names.join(' ') }
|
||||
end
|
||||
|
||||
def tracks_version_no_on_record? = false
|
||||
end
|
||||
|
||||
@@ -20,22 +20,23 @@ class PostCreatePlan
|
||||
parent_post_ids = normalise_parent_post_ids
|
||||
validate_parent_post_ids!(parent_post_ids)
|
||||
|
||||
{ url: @attributes[:url],
|
||||
title: @attributes[:title].to_s,
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence,
|
||||
original_created_from: @attributes[:original_created_from].presence,
|
||||
original_created_before: @attributes[:original_created_before].presence,
|
||||
tags: serialised_tags(direct_tag_specs, tag_sections),
|
||||
display_tags: display_tags(direct_tag_specs, tag_sections),
|
||||
duration: @attributes[:duration].to_s,
|
||||
video_ms: video_ms,
|
||||
parent_post_ids: parent_post_ids.join(' '),
|
||||
direct_tag_specs: direct_tag_specs,
|
||||
default_tag_specs: default_tag_specs,
|
||||
snapshot_tag_specs: snapshot_tag_specs,
|
||||
post_tag_specs: post_tag_specs,
|
||||
tag_sections: tag_sections,
|
||||
normalised_parent_post_ids: parent_post_ids }
|
||||
{
|
||||
url: @attributes[:url],
|
||||
title: @attributes[:title].to_s,
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence,
|
||||
original_created_from: @attributes[:original_created_from].presence,
|
||||
original_created_before: @attributes[:original_created_before].presence,
|
||||
tags: serialised_tags(direct_tag_specs, tag_sections),
|
||||
display_tags: display_tags(direct_tag_specs, tag_sections),
|
||||
duration: @attributes[:duration].to_s,
|
||||
video_ms: video_ms,
|
||||
parent_post_ids: parent_post_ids.join(' '),
|
||||
direct_tag_specs: direct_tag_specs,
|
||||
default_tag_specs: default_tag_specs,
|
||||
snapshot_tag_specs: snapshot_tag_specs,
|
||||
post_tag_specs: post_tag_specs,
|
||||
tag_sections: tag_sections,
|
||||
normalised_parent_post_ids: parent_post_ids }
|
||||
end
|
||||
|
||||
private
|
||||
@@ -49,6 +50,7 @@ class PostCreatePlan
|
||||
tag_names.each do |raw_name|
|
||||
tag_name, category, sections = parse_raw_tag_name(raw_name)
|
||||
existing_tag = existing_tags_by_name[tag_name]
|
||||
raise Tag::NicoTagNormalisationError if existing_tag&.nico?
|
||||
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
|
||||
|
||||
direct_tag_specs << {
|
||||
@@ -137,14 +139,14 @@ class PostCreatePlan
|
||||
Tag.expand_parent_tags(existing_snapshot_tags)
|
||||
.reject(&:deprecated?)
|
||||
.map { |tag|
|
||||
{ name: tag.name,
|
||||
category: tag.category.to_sym }
|
||||
}
|
||||
{
|
||||
name: tag.name,
|
||||
category: tag.category.to_sym } }
|
||||
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
||||
end
|
||||
|
||||
def merge_tag_specs specs
|
||||
specs.each_with_object({ }) { |spec, merged|
|
||||
specs.each_with_object({ }) do |spec, merged|
|
||||
merged[spec[:name]] =
|
||||
if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general
|
||||
merged[spec[:name]]
|
||||
@@ -153,7 +155,7 @@ class PostCreatePlan
|
||||
name: spec[:name],
|
||||
category: spec[:category] }
|
||||
end
|
||||
}.values.sort_by { _1[:name] }
|
||||
end.values.sort_by { _1[:name] }
|
||||
end
|
||||
|
||||
def existing_tags_by_name
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
class TagVersioning
|
||||
def self.record! tag, event_type:, created_by_user:
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
if tag.nico?
|
||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
else
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
end
|
||||
end
|
||||
|
||||
def self.ensure_snapshot! tag, created_by_user:
|
||||
return if tag.tag_versions.exists?
|
||||
if tag.nico?
|
||||
return if tag.nico_tag_versions.exists?
|
||||
|
||||
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:)
|
||||
NicoTagVersionRecorder.record!(tag:, event_type: :create, created_by_user:)
|
||||
else
|
||||
return if tag.tag_versions.exists?
|
||||
|
||||
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:)
|
||||
end
|
||||
end
|
||||
|
||||
def self.record_tag_snapshot! tag, created_by_user:
|
||||
event_type = tag.tag_versions.exists? ? :update : :create
|
||||
event_type =
|
||||
if tag.nico?
|
||||
tag.nico_tag_versions.exists? ? :update : :create
|
||||
else
|
||||
tag.tag_versions.exists? ? :update : :create
|
||||
end
|
||||
|
||||
record!(tag, event_type:, created_by_user:)
|
||||
end
|
||||
|
||||
|
||||
@@ -47,14 +47,10 @@ class VersionRecorder
|
||||
end
|
||||
|
||||
def update_record_version_no! version_no
|
||||
return unless tracks_version_no_on_record?
|
||||
|
||||
@record.update_columns(version_no:)
|
||||
@record.version_no = version_no
|
||||
end
|
||||
|
||||
def tracks_version_no_on_record? = true
|
||||
|
||||
def validate_version_sequence! latest
|
||||
if !(latest) && @event_type != 'create'
|
||||
raise "#{ version_class.name } first event must be create"
|
||||
@@ -64,7 +60,7 @@ class VersionRecorder
|
||||
raise "#{ version_class.name } create event already exists"
|
||||
end
|
||||
|
||||
return if !(latest) || !(tracks_version_no_on_record?)
|
||||
return unless latest
|
||||
|
||||
if @record.version_no != latest.version_no
|
||||
raise ("#{ record_class.name }##{ @record.id } version_no is #{ @record.version_no }, " +
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,54 +0,0 @@
|
||||
class CreateExternalTags < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
create_table :external_tags do |t|
|
||||
t.string :platform, limit: 16, null: false
|
||||
t.string :name, limit: 255, null: false
|
||||
t.integer :post_count, null: false, default: 0
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [:platform, :name], unique: true
|
||||
end
|
||||
|
||||
execute <<~SQL
|
||||
INSERT INTO
|
||||
external_tags(id, platform, name, post_count, created_at)
|
||||
SELECT
|
||||
t.id
|
||||
, 'nico' AS platform
|
||||
, SUBSTR(tn.name, 6) AS name
|
||||
, t.post_count
|
||||
, t.created_at
|
||||
FROM
|
||||
tags t
|
||||
INNER JOIN
|
||||
tag_names tn
|
||||
ON
|
||||
tn.id = t.tag_name_id
|
||||
AND t.category = 'nico'
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
INSERT INTO
|
||||
external_tags(id, platform, name, post_count, created_at)
|
||||
SELECT
|
||||
ntv.tag_id
|
||||
, 'nico' AS platform
|
||||
, SUBSTR(ntv.name, 6) AS name
|
||||
, 0 AS post_count
|
||||
, ntv.created_at
|
||||
FROM
|
||||
nico_tag_versions ntv
|
||||
LEFT JOIN
|
||||
external_tags et
|
||||
ON
|
||||
et.id = ntv.tag_id
|
||||
WHERE
|
||||
ntv.version_no = 1
|
||||
AND et.id IS NULL
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :external_tags
|
||||
end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
class CreatePostExternalTags < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
create_table :post_external_tags, primary_key: [:post_id, :external_tag_id] do |t|
|
||||
t.references :post, null: false, index: false, foreign_key: true
|
||||
t.references :external_tag, null: false, foreign_key: true
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
|
||||
execute <<~SQL
|
||||
INSERT INTO
|
||||
post_external_tags(post_id, external_tag_id, created_at)
|
||||
SELECT
|
||||
pt.post_id
|
||||
, pt.tag_id AS external_tag_id
|
||||
, pt.created_at
|
||||
FROM
|
||||
post_tags pt
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
pt.tag_id = t.id
|
||||
AND t.category = 'nico'
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :post_external_tags
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
class ChangeForeignKeyOnNicoTagRelations < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
remove_foreign_key :nico_tag_relations, :tags, column: :nico_tag_id
|
||||
add_foreign_key :nico_tag_relations, :external_tags, column: :nico_tag_id
|
||||
end
|
||||
end
|
||||
@@ -1,199 +0,0 @@
|
||||
class MigrateExternalTags < ActiveRecord::Migration[8.0]
|
||||
class MigrationPostVersion < ActiveRecord::Base
|
||||
self.table_name = 'post_versions'
|
||||
end
|
||||
|
||||
def up
|
||||
x = connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
post_tag_sections pts
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.id = pts.tag_id
|
||||
WHERE
|
||||
t.category = 'nico'
|
||||
SQL
|
||||
if x > 0
|
||||
raise "post_tag_sections に #{ x } 件のチンカスがあります!"
|
||||
end
|
||||
|
||||
x = connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
materials m
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.id = m.tag_id
|
||||
WHERE
|
||||
t.category = 'nico'
|
||||
SQL
|
||||
if x > 0
|
||||
raise "materials に #{ x } 件のチンカスがあります!"
|
||||
end
|
||||
|
||||
x = connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
tag_implications ti
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.id = ti.tag_id
|
||||
OR t.id = ti.parent_tag_id
|
||||
WHERE
|
||||
t.category = 'nico'
|
||||
SQL
|
||||
if x > 0
|
||||
raise "tag_implications に #{ x } 件のチンカスがあります!"
|
||||
end
|
||||
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
ts
|
||||
FROM
|
||||
tag_similarities ts
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.category = 'nico'
|
||||
AND (t.id = ts.tag_id
|
||||
OR t.id = ts.target_tag_id)
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
tset
|
||||
FROM
|
||||
theatre_skip_event_tags tset
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.category = 'nico'
|
||||
AND t.id = tset.tag_id
|
||||
SQL
|
||||
|
||||
remove_check_constraint :post_versions, name: 'chk_post_versions_tags_json_schema'
|
||||
|
||||
say_with_time 'Migrate post_versions.tags_json' do
|
||||
count = 0
|
||||
|
||||
MigrationPostVersion.find_each(batch_size: 500) do |version|
|
||||
tags = version.tags_json.map do |tag|
|
||||
if tag.fetch('category') == 'nico'
|
||||
{ 'external_tag_id' => tag.fetch('id') }
|
||||
else
|
||||
{ 'tag_id' => tag.fetch('id'),
|
||||
'version_no' => tag.fetch('version_no'),
|
||||
'name' => tag.fetch('name'),
|
||||
'category' => tag.fetch('category'),
|
||||
'sections' => tag.fetch('sections') }
|
||||
end
|
||||
end
|
||||
|
||||
version.update_columns(tags_json: tags)
|
||||
count += 1
|
||||
end
|
||||
|
||||
count
|
||||
end
|
||||
|
||||
add_tags_json_constraint!
|
||||
|
||||
tag_name_ids = connection.select_values(<<~SQL)
|
||||
SELECT
|
||||
tag_name_id
|
||||
FROM
|
||||
tags
|
||||
WHERE
|
||||
category = 'nico'
|
||||
SQL
|
||||
|
||||
connection.transaction do
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
pt
|
||||
FROM
|
||||
post_tags pt
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.category = 'nico'
|
||||
AND t.id = pt.tag_id
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
FROM
|
||||
tags
|
||||
WHERE
|
||||
category = 'nico'
|
||||
SQL
|
||||
|
||||
unless tag_name_ids.empty?
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
FROM
|
||||
tag_names
|
||||
WHERE
|
||||
id IN (#{ tag_name_ids.join(', ') })
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_tags_json_constraint!
|
||||
schema = { type: 'array',
|
||||
items: { oneOf: [internal_tag_schema, external_tag_schema] } }
|
||||
|
||||
quoted_schema = connection.quote(JSON.generate(schema))
|
||||
|
||||
add_check_constraint :post_versions,
|
||||
"JSON_SCHEMA_VALID(#{ quoted_schema }, tags_json)",
|
||||
name: 'chk_post_versions_tags_json_schema'
|
||||
end
|
||||
|
||||
def internal_tag_schema
|
||||
{ type: 'object',
|
||||
properties: { tag_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'] },
|
||||
sections: sections_schema },
|
||||
required: ['tag_id', 'version_no', 'sections'],
|
||||
additionalProperties: false }
|
||||
end
|
||||
|
||||
def external_tag_schema
|
||||
{ type: 'object',
|
||||
properties: { external_tag_id: { type: 'integer', minimum: 1 } },
|
||||
required: ['external_tag_id'],
|
||||
additionalProperties: false }
|
||||
end
|
||||
|
||||
def sections_schema
|
||||
{ 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 } }
|
||||
end
|
||||
end
|
||||
生成ファイル
+10
-20
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) 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|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -48,14 +48,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||
t.index ["tag_id"], name: "index_deerjikists_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "external_tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "platform", limit: 16, null: false
|
||||
t.string "name", null: false
|
||||
t.integer "post_count", default: 0, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["platform", "name"], name: "index_external_tags_on_platform_and_name", unique: true
|
||||
end
|
||||
|
||||
create_table "gekanator_ai_runs", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "model", null: false
|
||||
t.integer "input_tokens", default: 0, null: false
|
||||
@@ -289,13 +281,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||
t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive"
|
||||
end
|
||||
|
||||
create_table "post_external_tags", primary_key: ["post_id", "external_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "post_id", null: false
|
||||
t.bigint "external_tag_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["external_tag_id"], name: "index_post_external_tags_on_external_tag_id"
|
||||
end
|
||||
|
||||
create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "post_id", null: false
|
||||
t.bigint "parent_post_id", null: false
|
||||
@@ -367,7 +352,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||
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\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"tag_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\"]},\"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\":[\"tag_id\",\"version_no\",\"sections\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"external_tag_id\":{\"type\":\"integer\",\"minimum\":1}},\"required\":[\"external_tag_id\"],\"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
|
||||
|
||||
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
@@ -424,7 +409,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||
t.bigint "canonical_id"
|
||||
t.datetime "created_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 ["discarded_at"], name: "index_tag_names_on_discarded_at"
|
||||
t.index ["name"], name: "index_tag_names_on_name", unique: true
|
||||
end
|
||||
|
||||
@@ -461,8 +448,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "post_count", default: 0, null: false
|
||||
t.datetime "discarded_at"
|
||||
t.integer "version_no", null: false
|
||||
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.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"
|
||||
@@ -709,17 +698,17 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||
add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id"
|
||||
add_foreign_key "material_versions", "materials"
|
||||
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: "updated_by_user_id"
|
||||
add_foreign_key "materials", "materials", column: "parent_id"
|
||||
add_foreign_key "materials", "tags"
|
||||
add_foreign_key "materials", "users", column: "created_by_user_id"
|
||||
add_foreign_key "materials", "users", column: "updated_by_user_id"
|
||||
add_foreign_key "nico_tag_relations", "external_tags", column: "nico_tag_id"
|
||||
add_foreign_key "nico_tag_relations", "tags"
|
||||
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 "post_external_tags", "external_tags"
|
||||
add_foreign_key "post_external_tags", "posts"
|
||||
add_foreign_key "post_implications", "posts"
|
||||
add_foreign_key "post_implications", "posts", column: "parent_post_id"
|
||||
add_foreign_key "post_similarities", "posts"
|
||||
@@ -737,6 +726,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||
add_foreign_key "tag_names", "tag_names", column: "canonical_id"
|
||||
add_foreign_key "tag_similarities", "tags"
|
||||
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 "tags", "tag_names"
|
||||
add_foreign_key "theatre_comments", "theatres"
|
||||
|
||||
@@ -30,28 +30,9 @@ namespace :nico do
|
||||
end
|
||||
end
|
||||
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each(&:destroy!)
|
||||
end
|
||||
|
||||
def sync_post_external_tags! post, desired_external_tag_ids, current_external_tag_ids: nil
|
||||
current_external_tag_ids ||=
|
||||
PostExternalTag.where(post_id: post.id).pluck(:external_tag_id).to_set
|
||||
desired_external_tag_ids = desired_external_tag_ids.compact.to_set
|
||||
|
||||
to_add = desired_external_tag_ids - current_external_tag_ids
|
||||
to_remove = current_external_tag_ids - desired_external_tag_ids
|
||||
|
||||
ExternalTag.where(id: to_add.to_a).find_each do |external_tag|
|
||||
begin
|
||||
PostExternalTag.create!(post:, external_tag:)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
;
|
||||
end
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each do |pt|
|
||||
pt.destroy!
|
||||
end
|
||||
|
||||
PostExternalTag
|
||||
.where(post_id: post.id, external_tag_id: to_remove.to_a)
|
||||
.find_each(&:destroy!)
|
||||
end
|
||||
|
||||
mysql_user = ENV['MYSQL_USER']
|
||||
@@ -130,63 +111,58 @@ namespace :nico do
|
||||
sync_post_tags!(post, [Tag.tagme.id, Tag.bot.id, Tag.niconico.id, Tag.video.id])
|
||||
end
|
||||
|
||||
tags = post.tags
|
||||
# 既存のタグ Id. 集合
|
||||
kept_tag_ids = post.tags.pluck(:id).to_set
|
||||
|
||||
# 既存の外部タグ Id. 集合
|
||||
kept_external_tag_ids = post.external_tags.pluck(:id).to_set
|
||||
|
||||
# 記載すべき外部タグ Id. のリスト
|
||||
desired_external_tag_ids = []
|
||||
kept_tag_ids = tags.pluck(:id).to_set
|
||||
# うち内部タグ Id. 集合
|
||||
kept_non_nico_tag_ids = tags.not_nico.pluck(:id).to_set
|
||||
|
||||
# 記載すべき外部タグ Id. および連携される内部タグ Id. のリスト
|
||||
desired_nico_tag_based_ids = []
|
||||
# 記載すべき内部タグ Id. のリスト
|
||||
desired_tag_ids = kept_tag_ids.to_a
|
||||
desired_non_nico_tag_ids = []
|
||||
|
||||
datum['tags'].each do |raw|
|
||||
tag = ExternalTag.find_or_create_by!(platform: :nico, name: raw)
|
||||
name = TagNameSanitisationRule.sanitise("nico:#{ raw }")
|
||||
tag = Tag.find_or_create_by_tag_name!(name, category: :nico)
|
||||
|
||||
unless tag.nico_tag_versions.exists?
|
||||
NicoTagVersionRecorder.record!(external_tag: tag,
|
||||
event_type: :create,
|
||||
created_by_user: nil)
|
||||
end
|
||||
event_type = tag.nico_tag_versions.exists? ? :update : :create
|
||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil)
|
||||
|
||||
desired_external_tag_ids << tag.id
|
||||
desired_nico_tag_based_ids << tag.id
|
||||
|
||||
# 新たに記載される外部タグと連携される内部タグを記載
|
||||
# 連携タグは記載すれども消除せず.
|
||||
unless tag.id.in?(kept_external_tag_ids)
|
||||
desired_tag_ids.concat(tag.linked_tags.pluck(:id))
|
||||
unless tag.id.in?(kept_tag_ids)
|
||||
linked_ids = tag.linked_tags.pluck(:id)
|
||||
desired_non_nico_tag_ids.concat(linked_ids)
|
||||
desired_nico_tag_based_ids.concat(linked_ids)
|
||||
end
|
||||
end
|
||||
|
||||
deerjikist = Deerjikist.find_by(platform: :nico, code: datum['user'])
|
||||
if deerjikist
|
||||
desired_tag_ids << deerjikist.tag_id
|
||||
elsif !(Tag.where(id: kept_tag_ids).where(category: :deerjikist).exists?)
|
||||
desired_tag_ids << Tag.no_deerjikist.id
|
||||
desired_non_nico_tag_ids << deerjikist.tag_id
|
||||
desired_nico_tag_based_ids << deerjikist.tag_id
|
||||
elsif !(Tag.where(id: kept_non_nico_tag_ids).where(category: :deerjikist).exists?)
|
||||
desired_non_nico_tag_ids << Tag.no_deerjikist.id
|
||||
desired_nico_tag_based_ids << Tag.no_deerjikist.id
|
||||
end
|
||||
|
||||
desired_external_tag_ids.uniq!
|
||||
desired_tag_ids.uniq!
|
||||
desired_nico_tag_based_ids.uniq!
|
||||
|
||||
# 外部タグの記載に際しては “bot 操作” タグを記載しなぃ.
|
||||
if kept_tag_ids != desired_tag_ids.to_set
|
||||
desired_tag_ids << Tag.bot.id
|
||||
desired_tag_ids.uniq!
|
||||
desired_all_tag_ids = kept_non_nico_tag_ids.to_a + desired_nico_tag_based_ids
|
||||
desired_non_nico_tag_ids.concat(kept_non_nico_tag_ids.to_a)
|
||||
desired_non_nico_tag_ids.uniq!
|
||||
if kept_non_nico_tag_ids != desired_non_nico_tag_ids.to_set
|
||||
desired_all_tag_ids << Tag.bot.id
|
||||
end
|
||||
desired_all_tag_ids.uniq!
|
||||
|
||||
tags_changed =
|
||||
kept_tag_ids != desired_tag_ids.to_set ||
|
||||
kept_external_tag_ids != desired_external_tag_ids.to_set
|
||||
|
||||
sync_post_tags!(post, desired_tag_ids, current_tag_ids: kept_tag_ids)
|
||||
sync_post_external_tags!(post, desired_external_tag_ids,
|
||||
current_external_tag_ids: kept_external_tag_ids)
|
||||
sync_post_tags!(post, desired_all_tag_ids, current_tag_ids: kept_tag_ids)
|
||||
|
||||
if post_created
|
||||
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: nil)
|
||||
elsif post_changed || tags_changed
|
||||
elsif post_changed || kept_tag_ids != desired_all_tag_ids.to_set
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: nil)
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -57,7 +57,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
|
||||
it 'deletes the source tag_name' do
|
||||
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')
|
||||
end
|
||||
end
|
||||
@@ -75,27 +75,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
described_class.apply!
|
||||
expected_tag_name_id = existing.canonical_id || existing.id
|
||||
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
|
||||
expect(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)
|
||||
expect(TagName.exists?(source_tag_name_id)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -112,15 +92,13 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
end
|
||||
|
||||
it 'merges the source tag into the existing tag and deletes the source tag_name' do
|
||||
post = create(:post)
|
||||
PostTag.create!(post:, tag: source_tag)
|
||||
expect(TagName.find_by(name: 'foobar')&.tag&.id).to eq(existing_tag.id)
|
||||
expect(TagName.find_by(name: 'foo_bar')&.tag&.id).to eq(source_tag.id)
|
||||
|
||||
described_class.apply!
|
||||
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||
expect(post.reload.tags).to contain_exactly(existing_tag)
|
||||
expect(existing_tag.reload.name).to eq('foobar')
|
||||
expect(Tag.exists?(source_tag.id)).to be(false)
|
||||
expect(TagName.exists?(source_tag.tag_name_id)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+24
-152
@@ -173,47 +173,6 @@ RSpec.describe Tag, type: :model do
|
||||
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
|
||||
let!(:target_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
|
||||
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])
|
||||
|
||||
target_link = PostTag.find_by(post: post_record, tag: target_tag)
|
||||
|
||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
||||
expect(target_link).to be_present
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(source_tag.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(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
@@ -256,76 +218,17 @@ RSpec.describe Tag, type: :model do
|
||||
|
||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
||||
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(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(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'keeps source history and records the new target alias after deleting the source' do
|
||||
user = create_member_user!
|
||||
source_name = source_tag.name
|
||||
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name)
|
||||
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
|
||||
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)
|
||||
|
||||
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(target_link).to be_present
|
||||
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
|
||||
|
||||
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])
|
||||
|
||||
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(target_link).to be_present
|
||||
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
|
||||
expect(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -386,23 +288,17 @@ RSpec.describe Tag, type: :model do
|
||||
message: 'init')
|
||||
end
|
||||
|
||||
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
|
||||
it 'rolls back the transaction' do
|
||||
source_section = create(:post_tag_section, post: post_record,
|
||||
tag: source_tag,
|
||||
begin_ms: 1000, end_ms: 2000)
|
||||
|
||||
expect {
|
||||
described_class.merge_tags!(target_tag, [earlier_source, source_tag])
|
||||
described_class.merge_tags!(target_tag, [source_tag])
|
||||
}.to raise_error(ActiveRecord::RecordInvalid)
|
||||
|
||||
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
|
||||
expect(earlier_name.reload.canonical_id).to be_nil
|
||||
expect(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(Tag.with_discarded.find(source_tag.id)).not_to be_discarded
|
||||
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
|
||||
expect(source_post_tag.reload.tag_id).to eq(source_tag.id)
|
||||
expect(source_post_tag.sections).to contain_exactly(source_section)
|
||||
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
|
||||
@@ -413,45 +309,21 @@ RSpec.describe Tag, type: :model do
|
||||
end
|
||||
|
||||
context 'when merging a nico source tag' do
|
||||
let!(:target_tag) do
|
||||
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!(:target_tag) { create(:tag, category: :nico, name: 'nico:foo') }
|
||||
let!(:source_tag) { create(:tag, category: :nico, name: 'nico:bar') }
|
||||
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])
|
||||
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||
discarded_source_tag = Tag.with_discarded.find(source_tag.id)
|
||||
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)
|
||||
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
|
||||
|
||||
def snapshot_tags(post)
|
||||
|
||||
@@ -1252,7 +1252,9 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
context 'when nico tag already exists in tags' 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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
it 'returns 422 with tag field errors' do
|
||||
|
||||
@@ -8,7 +8,8 @@ RSpec.describe 'nico:sync' do
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def link_nico_to_tag!(nico_tag, tag)
|
||||
@@ -378,119 +379,4 @@ RSpec.describe 'nico:sync' do
|
||||
expect(versions.second.title).to eq('changed title')
|
||||
expect(versions.second.tags).to eq(snapshot_tags(post.reload))
|
||||
end
|
||||
|
||||
def create_external_tag!(name)
|
||||
ExternalTag.create!(platform: :nico, name:)
|
||||
end
|
||||
|
||||
def create_nico_sync_post!
|
||||
post = Post.create!(
|
||||
title: 't',
|
||||
url: 'https://www.nicovideo.jp/watch/sm9',
|
||||
uploaded_user: nil
|
||||
)
|
||||
|
||||
PostTag.create!(post:, tag: Tag.no_deerjikist)
|
||||
|
||||
post
|
||||
end
|
||||
|
||||
def run_nico_sync_with_tags! tags
|
||||
stub_python([{
|
||||
'code' => 'sm9',
|
||||
'title' => 't',
|
||||
'tags' => tags,
|
||||
'user' => nil
|
||||
}])
|
||||
|
||||
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
|
||||
|
||||
run_rake_task('nico:sync')
|
||||
end
|
||||
|
||||
it '外部タグが新規記載されたとき,その時点の連携タグを記載する' do
|
||||
post = create_nico_sync_post!
|
||||
|
||||
external_tag = create_external_tag!('AAA')
|
||||
linked_tag = create_tag!('spec_linked', category: :general)
|
||||
link_nico_to_tag!(external_tag, linked_tag)
|
||||
|
||||
run_nico_sync_with_tags!(['AAA'])
|
||||
|
||||
expect(post.reload.external_tags).to include(external_tag)
|
||||
expect(post.tags).to include(linked_tag)
|
||||
end
|
||||
|
||||
it '外部タグに差分がない場合,内外マッピングが変はっても連携タグを再評価しない' do
|
||||
post = create_nico_sync_post!
|
||||
|
||||
external_tag = create_external_tag!('AAA')
|
||||
old_linked_tag = create_tag!('spec_old_linked', category: :general)
|
||||
new_linked_tag = create_tag!('spec_new_linked', category: :general)
|
||||
|
||||
relation = link_nico_to_tag!(external_tag, old_linked_tag)
|
||||
|
||||
run_nico_sync_with_tags!(['AAA'])
|
||||
|
||||
expect(post.reload.tags).to include(old_linked_tag)
|
||||
|
||||
# 人手で連携タグを消除する.
|
||||
PostTag.find_by!(post:, tag: old_linked_tag).destroy!
|
||||
|
||||
# 内外マッピングを変更する.
|
||||
relation.destroy!
|
||||
link_nico_to_tag!(external_tag, new_linked_tag)
|
||||
|
||||
# 外部タグ自体には差分が無い.
|
||||
run_nico_sync_with_tags!(['AAA'])
|
||||
|
||||
post.reload
|
||||
|
||||
expect(post.external_tags).to include(external_tag)
|
||||
expect(post.tags).not_to include(old_linked_tag)
|
||||
expect(post.tags).not_to include(new_linked_tag)
|
||||
end
|
||||
|
||||
it '外部タグの消除では連携タグを消除せず,再記載時にその時点の内外マッピングを適用する' do
|
||||
post = create_nico_sync_post!
|
||||
|
||||
external_tag = create_external_tag!('AAA')
|
||||
old_linked_tag = create_tag!('spec_old_linked', category: :general)
|
||||
new_linked_tag = create_tag!('spec_new_linked', category: :general)
|
||||
|
||||
relation = link_nico_to_tag!(external_tag, old_linked_tag)
|
||||
|
||||
# 外部タグを新規記載する.
|
||||
run_nico_sync_with_tags!(['AAA'])
|
||||
|
||||
post.reload
|
||||
expect(post.external_tags).to include(external_tag)
|
||||
expect(post.tags).to include(old_linked_tag)
|
||||
|
||||
# 外部タグを消除する.
|
||||
run_nico_sync_with_tags!([])
|
||||
|
||||
post.reload
|
||||
expect(post.external_tags).not_to include(external_tag)
|
||||
|
||||
# 外部タグの消除によって連携タグまでは消除されない.
|
||||
expect(post.tags).to include(old_linked_tag)
|
||||
|
||||
# 外部タグが記載されてゐない間に内外マッピングを変更する.
|
||||
relation.destroy!
|
||||
link_nico_to_tag!(external_tag, new_linked_tag)
|
||||
|
||||
# 同じ外部タグを再記載する.
|
||||
run_nico_sync_with_tags!(['AAA'])
|
||||
|
||||
post.reload
|
||||
|
||||
expect(post.external_tags).to include(external_tag)
|
||||
|
||||
# 旧連携タグは自動的には消除されない.
|
||||
expect(post.tags).to include(old_linked_tag)
|
||||
|
||||
# 再記載時点の内外マッピングが新たに適用される.
|
||||
expect(post.tags).to include(new_linked_tag)
|
||||
end
|
||||
end
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする