コミットを比較

..
7 コミット
33個のファイルの変更430行の追加699行の削除
+2
ファイルの表示
@@ -92,4 +92,6 @@ class ApplicationController < ActionController::API
value value
end end
def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese
end end
+4 -5
ファイルの表示
@@ -72,6 +72,7 @@ class MaterialsController < ApplicationController
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member? return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
tag_name_raw = params[:tag].to_s.strip tag_name_raw = params[:tag].to_s.strip
file = params[:file] file = params[:file]
file_sha256 = MaterialFileSha256.from_upload(file) file_sha256 = MaterialFileSha256.from_upload(file)
@@ -89,7 +90,7 @@ class MaterialsController < ApplicationController
begin begin
Material.transaction do Material.transaction do
tag = resolve_material_tag!(tag_name_raw) tag = resolve_material_tag!(locale, tag_name_raw)
material = Material.new(tag:, url:, material = Material.new(tag:, url:,
created_by_user: current_user, created_by_user: current_user,
updated_by_user: current_user) updated_by_user: current_user)
@@ -236,10 +237,8 @@ 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_or_create_by!(name: tag_name_raw) Tag.find_or_create_by_tag_name!(locale, tag_name_raw, category: :material)
tag = tag_name.tag
tag || Tag.create!(tag_name:, category: :material)
end end
def material_index_needs_tag_name? filters def material_index_needs_tag_name? filters
+23 -15
ファイルの表示
@@ -15,22 +15,23 @@ class NicoTagsController < ApplicationController
limit = 1 if limit < 1 limit = 1 if limit < 1
post_tag_max_sql = post_tag_max_sql =
PostExternalTag PostTag
.select('external_tag_id, MAX(created_at) AS max_created_at') .select('tag_id, MAX(created_at) AS max_created_at')
.group('external_tag_id') .group('tag_id')
.to_sql .to_sql
q = q = Tag.nico_tags
ExternalTag .joins(:tag_name)
.joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \ .joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \
'ON post_tag_max.external_tag_id = external_tags.id') 'ON post_tag_max.tag_id = tags.id')
q = q.where('external_tags.name LIKE ?', "%#{ name }%") if name .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 if linked_tag
linked_tag_ids = linked_tag_ids =
Tag Tag
.joins(:tag_name) .joins(:tag_name)
.where('tag_names.name LIKE ?', "%#{ linked_tag }%") .where('tag_names.name LIKE ?', "%#{ linked_tag }%")
.pluck(:id) .pluck(:id)
linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id) linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id)
q = q.where(id: linked_nico_tag_ids) q = q.where(id: linked_nico_tag_ids)
end end
@@ -69,16 +70,23 @@ class NicoTagsController < ApplicationController
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member? return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
id = params[:id].to_i id = params[:id].to_i
tag = ExternalTag.find(id) tag = Tag.find(id)
return render_bad_request('ニコニコ・タグを指定してください.') unless tag.nico?
linked_tag_names = params[:tags].to_s.split linked_tag_names = params[:tags].to_s.split
linked_tags = nil linked_tags = nil
ApplicationRecord.transaction do ApplicationRecord.transaction do
linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false, linked_tags = Tag.normalise_tags!(locale, linked_tag_names,
with_no_deerjikist: false) 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) TagVersioning.record_tag_snapshots!(linked_tags, created_by_user: current_user)
+14 -11
ファイルの表示
@@ -27,8 +27,8 @@ class PostVersionsController < ApplicationController
'prev.original_created_before AS prev_original_created_before') 'prev.original_created_before AS prev_original_created_before')
q = q.where('post_versions.post_id = ?', post_id) if post_id q = q.where('post_versions.post_id = ?', post_id) if post_id
if tag_id if tag_id
q = q.where("JSON_CONTAINS(post_versions.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('tag_id', #{ tag_id }))") "OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('id', #{ tag_id }))")
end end
count = q.except(:select, :order, :limit, :offset).count count = q.except(:select, :order, :limit, :offset).count
@@ -113,18 +113,21 @@ class PostVersionsController < ApplicationController
end end
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| (cur_tags | prev_tags).map do |name|
type = type =
if cur_tags.include?(name) && prev_tags.include?(name) if cur_tags.include?(name) && prev_tags.include?(name)
'context' 'context'
elsif cur_tags.include?(name) elsif cur_tags.include?(name)
'added' 'added'
else else
'removed' 'removed'
end end
{ name:, type: } {
name:,
type:
}
end end
end end
end end
+17 -9
ファイルの表示
@@ -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
@@ -671,6 +673,7 @@ class PostsController < ApplicationController
post post
.post_tags .post_tags
.joins(tag: :tag_name) .joins(tag: :tag_name)
.merge(Tag.not_nico)
.merge(Tag.where(deprecated_at: nil)) .merge(Tag.where(deprecated_at: nil))
.includes(:sections, tag: :tag_name) .includes(:sections, tag: :tag_name)
.order('tag_names.name') .order('tag_names.name')
@@ -684,11 +687,13 @@ 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_sections: true) => { tags:, sections: } with_tagme: false,
deny_deprecated: true,
with_sections: true) => { tags:, sections: }
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?) tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:) video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:)
@@ -823,7 +828,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],
@@ -831,12 +836,15 @@ 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], Tag.normalise_tags!(locale, snapshot[:tag_names],
with_tagme: false, with_tagme: false,
deny_deprecated: true, deny_deprecated: true,
with_sections: true) => { tags:, sections: } with_sections: true) => { tags: editable_tags, sections: }
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user) 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?) tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
post.video_ms = tags.any? { _1.id == Tag.video.id } ? snapshot[:video_ms] : nil post.video_ms = tags.any? { _1.id == Tag.video.id } ? snapshot[:video_ms] : nil
+2
ファイルの表示
@@ -10,6 +10,7 @@ class TagChildrenController < ApplicationController
parent = Tag.find(parent_id) parent = Tag.find(parent_id)
child = Tag.find(child_id) child = Tag.find(child_id)
return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico?
ApplicationRecord.transaction do ApplicationRecord.transaction do
TagVersioning.ensure_snapshot!(child, created_by_user: current_user) TagVersioning.ensure_snapshot!(child, created_by_user: current_user)
@@ -32,6 +33,7 @@ class TagChildrenController < ApplicationController
parent = Tag.find(parent_id) parent = Tag.find(parent_id)
child = Tag.find(child_id) child = Tag.find(child_id)
return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico?
ApplicationRecord.transaction do ApplicationRecord.transaction do
TagVersioning.ensure_snapshot!(child, created_by_user: current_user) TagVersioning.ensure_snapshot!(child, created_by_user: current_user)
+38 -50
ファイルの表示
@@ -100,14 +100,13 @@ class TagsController < ApplicationController
def autocomplete def autocomplete
q = params[:q].to_s.strip.sub(/\Anot:/i, '') q = params[:q].to_s.strip.sub(/\Anot:/i, '')
prefix = "#{ ActiveRecord::Base.sanitize_sql_like(q) }%"
with_nico = bool?(:nico, default: true) with_nico = bool?(:nico, default: true)
present_only = bool?(:present, default: true) present_only = bool?(:present, default: true)
alias_rows = alias_rows =
TagName TagName
.where('name LIKE ?', prefix) .where('name LIKE ?', "#{ q }%")
.where.not(canonical_id: nil) .where.not(canonical_id: nil)
.pluck(:canonical_id, :name) .pluck(:canonical_id, :name)
@@ -119,57 +118,29 @@ class TagsController < ApplicationController
matched_alias_by_tag_name_id[canonical_id] ||= alias_name matched_alias_by_tag_name_id[canonical_id] ||= alias_name
end end
base = base = Tag.joins(:tag_name)
Tag .includes(:tag_name, :materials, tag_name: :wiki_page)
.joins(:tag_name) .where(deprecated_at: nil)
.includes(:tag_name, :materials, tag_name: :wiki_page)
.where(deprecated_at: nil)
base = base.where('tags.post_count > 0') if present_only 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 = tags =
if with_nico if canonical_ids.present?
canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq)) canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq))
else else
canonical_hit canonical_hit
end end
internal_rows = tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a
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])
}
return render json: internal_rows unless with_nico render json: tags.map { |tag|
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
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
end end
def show def show
@@ -307,11 +278,17 @@ class TagsController < ApplicationController
return unless validate_tag_rename(tag, name) return unless validate_tag_rename(tag, name)
locale = resolve_locale!
alias_names = params[:aliases].to_s.split.uniq alias_names = params[:aliases].to_s.split.uniq
parent_names = params[:parent_tags].to_s.split.uniq parent_names = params[:parent_tags].to_s.split.uniq
deprecated = bool?(:deprecated) deprecated = bool?(:deprecated)
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 return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
end end
@@ -333,7 +310,7 @@ class TagsController < ApplicationController
alias_names.delete(name) alias_names.delete(name)
update_aliases!(tag, alias_names) update_aliases!(tag, alias_names)
update_parent_tags!(tag, parent_names) update_parent_tags!(locale, tag, parent_names)
tag.reload tag.reload
@@ -359,9 +336,13 @@ class TagsController < ApplicationController
tag = Tag.find(params[:id]) 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) 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 return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
end end
@@ -568,6 +549,11 @@ class TagsController < ApplicationController
end end
def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil 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:) TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
return unless name_changed return unless name_changed
@@ -675,9 +661,11 @@ class TagsController < ApplicationController
end end
end end
def update_parent_tags! tag, parent_names def update_parent_tags! locale, tag, parent_names
parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false, parent_tags = Tag.normalise_tags!(locale, parent_names,
with_no_deerjikist: false) with_tagme: false,
with_no_deerjikist: false,
deny_nico: true)
old_parent_tags = tag.parents.to_a old_parent_tags = tag.parents.to_a
-20
ファイルの表示
@@ -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
+3
ファイルの表示
@@ -0,0 +1,3 @@
class Language < ApplicationRecord
has_many :languages, class_name: 'Locale', foreign_key: :language_code
end
+24
ファイルの表示
@@ -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
+2 -5
ファイルの表示
@@ -1,9 +1,6 @@
class NicoTagRelation < ApplicationRecord class NicoTagRelation < ApplicationRecord
belongs_to :nico_tag, belongs_to :nico_tag, class_name: 'Tag'
class_name: 'ExternalTag', belongs_to :tag, class_name: 'Tag'
foreign_key: :nico_tag_id,
inverse_of: :nico_tag_relations
belongs_to :tag, class_name: 'Tag', foreign_key: :tag_id
validates :nico_tag_id, presence: true validates :nico_tag_id, presence: true
validates :tag_id, presence: true validates :tag_id, presence: true
+1 -1
ファイルの表示
@@ -1,7 +1,7 @@
class NicoTagVersion < ApplicationRecord class NicoTagVersion < ApplicationRecord
include VersionRecord include VersionRecord
belongs_to :external_tag, foreign_key: :tag_id, inverse_of: :nico_tag_versions belongs_to :tag
validates :name, presence: true validates :name, presence: true
end end
+13 -24
ファイルの表示
@@ -92,9 +92,6 @@ class Post < ApplicationRecord
inverse_of: :parent_post inverse_of: :parent_post
has_many :children, through: :child_post_implications, source: :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 has_one_attached :thumbnail
attribute :version_no, :integer, default: 1 attribute :version_no, :integer, default: 1
@@ -153,27 +150,19 @@ class Post < ApplicationRecord
end end
def snapshot_tags_json def snapshot_tags_json
tag_snapshots = post_tags
post_tags .joins(tag: :tag_name)
.joins(tag: :tag_name) .includes(:sections, tag: :tag_name)
.includes(:sections, tag: :tag_name) .order('tags.id')
.order('tags.id') .map do |pt|
.map { |pt| { 'id' => pt.tag.id,
{ 'tag_id' => pt.tag.id, 'version_no' => pt.tag.version_no,
'version_no' => pt.tag.version_no, 'name' => pt.tag.name,
'name' => pt.tag.name, 'category' => pt.tag.category,
'category' => pt.tag.category, 'sections' => pt.sections.sort_by(&:begin_ms).map {
'sections' => pt.sections.sort_by(&:begin_ms).map { { 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms } } }
} } end
}
external_tag_snapshots =
post_external_tags.order(:external_tag_id).map {
{ 'external_tag_id' => _1.external_tag_id }
}
tag_snapshots + external_tag_snapshots
end end
def self.section_literal section def self.section_literal section
-4
ファイルの表示
@@ -1,4 +0,0 @@
class PostExternalTag < ApplicationRecord
belongs_to :post
belongs_to :external_tag
end
+3
ファイルの表示
@@ -0,0 +1,3 @@
class Script < ApplicationRecord
;
end
+59 -27
ファイルの表示
@@ -28,10 +28,11 @@ class Tag < ApplicationRecord
has_many :post_tags, inverse_of: :tag has_many :post_tags, inverse_of: :tag
has_many :posts, through: :post_tags 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, has_many :reversed_nico_tag_relations,
class_name: 'NicoTagRelation', class_name: 'NicoTagRelation', foreign_key: :tag_id, dependent: :destroy
foreign_key: :nico_tag_id,
dependent: :destroy
has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag
has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy
@@ -49,7 +50,9 @@ class Tag < ApplicationRecord
has_many :materials has_many :materials
has_many :tag_versions has_many :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
@@ -63,15 +66,17 @@ class Tag < ApplicationRecord
character: 'character', character: 'character',
general: 'general', general: 'general',
material: 'material', material: 'material',
nico: 'nico',
meta: 'meta' meta: 'meta'
validates :category, presence: true, inclusion: { in: Tag.categories.keys } 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 :tag_name_must_be_canonical
validate :category_must_be_deerjikist_with_deerjikists 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 = { CATEGORY_PREFIXES = {
'general:' => :general, 'general:' => :general,
@@ -97,18 +102,21 @@ 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_deprecated: false, with_no_deerjikist: true,
with_sections: false deny_nico: true,
if tag_names.any? { |n| n.downcase.start_with?('nico:') } deny_deprecated: false,
with_sections: false
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
raise NicoTagNormalisationError raise NicoTagNormalisationError
end end
@@ -136,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
@@ -224,13 +232,25 @@ 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_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_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
if tag
tag.update!(tag_name_id: tn.id) if tag.tag_name_id != tn.id
return tag
end
tag = Tag.create!(tag_name: tn, category:)
tn.update!(tag:)
tag
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
retry retry
end end
@@ -268,8 +288,12 @@ class Tag < ApplicationRecord
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:) TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
source_tag.destroy! source_tag.destroy!
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id, if source_tag.nico?
updated_at: Time.current) source_tag_name.destroy!
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:) TagVersioning.record!(target_tag, event_type: :update, created_by_user:)
end end
@@ -296,9 +320,11 @@ class Tag < ApplicationRecord
private private
def tag_name_mustnt_start_with_nico def nico_tag_name_must_start_with_nico
if name.to_s.downcase.start_with?('nico:') n = name.to_s
errors.add :name, 'タグの命名規則に反してゐます.' if ((nico? && !(n.downcase.start_with?('nico:'))) ||
(!(nico?) && n.downcase.start_with?('nico:')))
errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.'
end end
end end
@@ -339,4 +365,10 @@ class Tag < ApplicationRecord
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
end end
def nico_tags_cannot_be_deprecated
if nico? && deprecated_at.present?
errors.add :deprecated_at, 'ニコタグは廃止できません.'
end
end
end end
+7 -1
ファイルの表示
@@ -1,5 +1,5 @@
class TagName < ApplicationRecord class TagName < ApplicationRecord
has_one :tag belongs_to :tag, optional: true
has_one :wiki_page has_one :wiki_page
belongs_to :canonical, class_name: 'TagName', optional: true belongs_to :canonical, class_name: 'TagName', optional: true
@@ -21,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
+6 -9
ファイルの表示
@@ -1,22 +1,19 @@
class NicoTagVersionRecorder < VersionRecorder class NicoTagVersionRecorder < VersionRecorder
def self.record! external_tag:, event_type:, created_by_user: def self.record! tag:, event_type:, created_by_user:
new(external_tag:, event_type:, created_by_user:).record! new(tag:, event_type:, created_by_user:).record!
end end
def initialize external_tag:, event_type:, created_by_user: def initialize tag:, event_type:, created_by_user:
super(record: external_tag, event_type:, created_by_user:) super(record: tag, event_type:, created_by_user:)
end end
private private
def version_class = NicoTagVersion def version_class = NicoTagVersion
def version_association = :nico_tag_versions def version_association = :nico_tag_versions
def record_key = :external_tag def record_key = :tag
def snapshot_attributes def snapshot_attributes
{ name: "#{ @record.platform }:#{ @record.name }", { name: @record.name, linked_tags: @record.snapshot_linked_tag_names.join(' ') }
linked_tags: @record.snapshot_linked_tag_names.join(' ') }
end end
def tracks_version_no_on_record? = false
end end
+23 -21
ファイルの表示
@@ -20,22 +20,23 @@ class PostCreatePlan
parent_post_ids = normalise_parent_post_ids parent_post_ids = normalise_parent_post_ids
validate_parent_post_ids!(parent_post_ids) validate_parent_post_ids!(parent_post_ids)
{ url: @attributes[:url], {
title: @attributes[:title].to_s, url: @attributes[:url],
thumbnail_base: @attributes[:thumbnail_base].presence, title: @attributes[:title].to_s,
original_created_from: @attributes[:original_created_from].presence, thumbnail_base: @attributes[:thumbnail_base].presence,
original_created_before: @attributes[:original_created_before].presence, original_created_from: @attributes[:original_created_from].presence,
tags: serialised_tags(direct_tag_specs, tag_sections), original_created_before: @attributes[:original_created_before].presence,
display_tags: display_tags(direct_tag_specs, tag_sections), tags: serialised_tags(direct_tag_specs, tag_sections),
duration: @attributes[:duration].to_s, display_tags: display_tags(direct_tag_specs, tag_sections),
video_ms: video_ms, duration: @attributes[:duration].to_s,
parent_post_ids: parent_post_ids.join(' '), video_ms: video_ms,
direct_tag_specs: direct_tag_specs, parent_post_ids: parent_post_ids.join(' '),
default_tag_specs: default_tag_specs, direct_tag_specs: direct_tag_specs,
snapshot_tag_specs: snapshot_tag_specs, default_tag_specs: default_tag_specs,
post_tag_specs: post_tag_specs, snapshot_tag_specs: snapshot_tag_specs,
tag_sections: tag_sections, post_tag_specs: post_tag_specs,
normalised_parent_post_ids: parent_post_ids } tag_sections: tag_sections,
normalised_parent_post_ids: parent_post_ids }
end end
private private
@@ -49,6 +50,7 @@ class PostCreatePlan
tag_names.each do |raw_name| tag_names.each do |raw_name|
tag_name, category, sections = parse_raw_tag_name(raw_name) tag_name, category, sections = parse_raw_tag_name(raw_name)
existing_tag = existing_tags_by_name[tag_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? raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
direct_tag_specs << { direct_tag_specs << {
@@ -137,14 +139,14 @@ class PostCreatePlan
Tag.expand_parent_tags(existing_snapshot_tags) Tag.expand_parent_tags(existing_snapshot_tags)
.reject(&:deprecated?) .reject(&:deprecated?)
.map { |tag| .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) merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
end end
def merge_tag_specs specs def merge_tag_specs specs
specs.each_with_object({ }) { |spec, merged| specs.each_with_object({ }) do |spec, merged|
merged[spec[:name]] = merged[spec[:name]] =
if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general
merged[spec[:name]] merged[spec[:name]]
@@ -153,7 +155,7 @@ class PostCreatePlan
name: spec[:name], name: spec[:name],
category: spec[:category] } category: spec[:category] }
end end
}.values.sort_by { _1[:name] } end.values.sort_by { _1[:name] }
end end
def existing_tags_by_name def existing_tags_by_name
+9 -9
ファイルの表示
@@ -9,7 +9,7 @@ class PostCreator
@field_warnings = { } @field_warnings = { }
end end
def create! def create!(locale)
thumbnail_attachment = prepare_thumbnail_attachment thumbnail_attachment = prepare_thumbnail_attachment
post = Post.new(title: @attributes[:title].presence, post = Post.new(title: @attributes[:title].presence,
url: @attributes[:url], url: @attributes[:url],
@@ -25,7 +25,7 @@ class PostCreator
post_tags = planned_post_tags post_tags = planned_post_tags
sections = planned_sections sections = planned_sections
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor) TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
post.video_ms = planned_video_ms post.video_ms = planned_video_ms(locale)
post.save! post.save!
sync_post_tags!(post, post_tags, sections) sync_post_tags!(post, post_tags, sections)
sync_parent_posts!(post, planned_parent_post_ids) sync_parent_posts!(post, planned_parent_post_ids)
@@ -53,15 +53,15 @@ class PostCreator
def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids] def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]
def planned_video_ms def planned_video_ms locale
planned_create_attributes[:video_ms] planned_create_attributes(locale)[:video_ms]
end end
def planned_create_attributes def planned_create_attributes locale
@planned_create_attributes ||= begin @planned_create_attributes ||= begin
if @attributes.key?(:snapshot_tag_specs) if @attributes.key?(:snapshot_tag_specs)
snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || []) snapshot_tags = materialise_tags(locale, @attributes[:snapshot_tag_specs] || [])
post_tags = materialise_tags(@attributes[:post_tag_specs] || []) post_tags = materialise_tags(locale, @attributes[:post_tag_specs] || [])
{ {
snapshot_tags: snapshot_tags, snapshot_tags: snapshot_tags,
post_tags: post_tags, post_tags: post_tags,
@@ -92,13 +92,13 @@ class PostCreator
video_ms: plan[:video_ms] } video_ms: plan[:video_ms] }
end end
def materialise_tags specs def materialise_tags locale, specs
Array(specs).each_with_object({ }) do |spec, tags| Array(specs).each_with_object({ }) do |spec, tags|
name = spec[:name] || spec['name'] name = spec[:name] || spec['name']
category = spec[:category] || spec['category'] category = spec[:category] || spec['category']
next if name.blank? || category.blank? next if name.blank? || category.blank?
tag = Tag.find_or_create_by_tag_name!(name, category:) tag = Tag.find_or_create_by_tag_name!(locale, name, category:)
tag.update!(category:) if tag.category.to_sym != category.to_sym tag.update!(category:) if tag.category.to_sym != category.to_sym
tags[name] ||= tag tags[name] ||= tag
end.values end.values
+20 -4
ファイルの表示
@@ -1,16 +1,32 @@
class TagVersioning class TagVersioning
def self.record! tag, event_type:, created_by_user: 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 end
def self.ensure_snapshot! tag, created_by_user: 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 end
def self.record_tag_snapshot! tag, created_by_user: 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:) record!(tag, event_type:, created_by_user:)
end end
+1 -5
ファイルの表示
@@ -47,14 +47,10 @@ class VersionRecorder
end end
def update_record_version_no! version_no def update_record_version_no! version_no
return unless tracks_version_no_on_record?
@record.update_columns(version_no:) @record.update_columns(version_no:)
@record.version_no = version_no @record.version_no = version_no
end end
def tracks_version_no_on_record? = true
def validate_version_sequence! latest def validate_version_sequence! latest
if !(latest) && @event_type != 'create' if !(latest) && @event_type != 'create'
raise "#{ version_class.name } first event must be create" raise "#{ version_class.name } first event must be create"
@@ -64,7 +60,7 @@ class VersionRecorder
raise "#{ version_class.name } create event already exists" raise "#{ version_class.name } create event already exists"
end end
return if !(latest) || !(tracks_version_no_on_record?) return unless latest
if @record.version_no != latest.version_no if @record.version_no != latest.version_no
raise ("#{ record_class.name }##{ @record.id } version_no is #{ @record.version_no }, " + raise ("#{ record_class.name }##{ @record.id } version_no is #{ @record.version_no }, " +
-54
ファイルの表示
@@ -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
-29
ファイルの表示
@@ -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
-6
ファイルの表示
@@ -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
-199
ファイルの表示
@@ -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
+20
ファイルの表示
@@ -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
+20
ファイルの表示
@@ -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
+25
ファイルの表示
@@ -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
+59
ファイルの表示
@@ -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
生成ファイル
+3 -20
ファイルの表示
@@ -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_230000) 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
@@ -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" t.index ["tag_id"], name: "index_deerjikists_on_tag_id"
end 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| create_table "gekanator_ai_runs", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "model", null: false t.string "model", null: false
t.integer "input_tokens", default: 0, 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" t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive"
end 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| 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 "post_id", null: false
t.bigint "parent_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 "(`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 "`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 "`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 end
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -715,11 +700,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
add_foreign_key "materials", "tags" add_foreign_key "materials", "tags"
add_foreign_key "materials", "users", column: "created_by_user_id" add_foreign_key "materials", "users", column: "created_by_user_id"
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", "external_tags", column: "nico_tag_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_versions", "users", column: "created_by_user_id" 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"
add_foreign_key "post_implications", "posts", column: "parent_post_id" add_foreign_key "post_implications", "posts", column: "parent_post_id"
add_foreign_key "post_similarities", "posts" add_foreign_key "post_similarities", "posts"
+32 -56
ファイルの表示
@@ -30,28 +30,9 @@ namespace :nico do
end end
end end
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each(&:destroy!) PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each do |pt|
end pt.destroy!
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
end end
PostExternalTag
.where(post_id: post.id, external_tag_id: to_remove.to_a)
.find_each(&:destroy!)
end end
mysql_user = ENV['MYSQL_USER'] 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]) sync_post_tags!(post, [Tag.tagme.id, Tag.bot.id, Tag.niconico.id, Tag.video.id])
end end
tags = post.tags
# 既存のタグ Id. 集合 # 既存のタグ Id. 集合
kept_tag_ids = post.tags.pluck(:id).to_set kept_tag_ids = tags.pluck(:id).to_set
# うち内部タグ Id. 集合
# 既存の外部タグ Id. 集合 kept_non_nico_tag_ids = tags.not_nico.pluck(:id).to_set
kept_external_tag_ids = post.external_tags.pluck(:id).to_set
# 記載すべき外部タグ Id. のリスト
desired_external_tag_ids = []
# 記載すべき外部タグ Id. および連携される内部タグ Id. のリスト
desired_nico_tag_based_ids = []
# 記載すべき内部タグ Id. のリスト # 記載すべき内部タグ Id. のリスト
desired_tag_ids = kept_tag_ids.to_a desired_non_nico_tag_ids = []
datum['tags'].each do |raw| 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!(Locale.nipponese, name, category: :nico)
unless tag.nico_tag_versions.exists? event_type = tag.nico_tag_versions.exists? ? :update : :create
NicoTagVersionRecorder.record!(external_tag: tag, NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil)
event_type: :create,
created_by_user: nil)
end
desired_external_tag_ids << tag.id desired_nico_tag_based_ids << tag.id
# 新たに記載される外部タグと連携される内部タグを記載 # 新たに記載される外部タグと連携される内部タグを記載
# 連携タグは記載すれども消除せず. unless tag.id.in?(kept_tag_ids)
unless tag.id.in?(kept_external_tag_ids) linked_ids = tag.linked_tags.pluck(:id)
desired_tag_ids.concat(tag.linked_tags.pluck(:id)) desired_non_nico_tag_ids.concat(linked_ids)
desired_nico_tag_based_ids.concat(linked_ids)
end end
end end
deerjikist = Deerjikist.find_by(platform: :nico, code: datum['user']) deerjikist = Deerjikist.find_by(platform: :nico, code: datum['user'])
if deerjikist if deerjikist
desired_tag_ids << deerjikist.tag_id desired_non_nico_tag_ids << deerjikist.tag_id
elsif !(Tag.where(id: kept_tag_ids).where(category: :deerjikist).exists?) desired_nico_tag_based_ids << deerjikist.tag_id
desired_tag_ids << Tag.no_deerjikist.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 end
desired_external_tag_ids.uniq! desired_nico_tag_based_ids.uniq!
desired_tag_ids.uniq!
# 外部タグの記載に際しては “bot 操作” タグを記載しなぃ. desired_all_tag_ids = kept_non_nico_tag_ids.to_a + desired_nico_tag_based_ids
if kept_tag_ids != desired_tag_ids.to_set desired_non_nico_tag_ids.concat(kept_non_nico_tag_ids.to_a)
desired_tag_ids << Tag.bot.id desired_non_nico_tag_ids.uniq!
desired_tag_ids.uniq! if kept_non_nico_tag_ids != desired_non_nico_tag_ids.to_set
desired_all_tag_ids << Tag.bot.id
end end
desired_all_tag_ids.uniq!
tags_changed = sync_post_tags!(post, desired_all_tag_ids, current_tag_ids: kept_tag_ids)
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)
if post_created if post_created
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: nil) 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.ensure_snapshot!(post, created_by_user: nil)
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil) PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil)
end end
-115
ファイルの表示
@@ -378,119 +378,4 @@ RSpec.describe 'nico:sync' do
expect(versions.second.title).to eq('changed title') expect(versions.second.title).to eq('changed title')
expect(versions.second.tags).to eq(snapshot_tags(post.reload)) expect(versions.second.tags).to eq(snapshot_tags(post.reload))
end 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 end