Reviewed-on: #421 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #421 でマージされました。
このコミットが含まれているのは:
@@ -14,7 +14,11 @@ class ExternalTag < ApplicationRecord
|
||||
|
||||
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')
|
||||
def snapshot_linked_tag_names language_code = 'ja'
|
||||
linked_tags
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.order('tag_names.name')
|
||||
.pluck('tag_names.name')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class Language < ApplicationRecord
|
||||
has_many :locales, foreign_key: :language_code, primary_key: :code
|
||||
|
||||
enum :code, ja: 'ja'
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
|
||||
enum :code, ja: 'ja', en: 'en', fr: 'fr'
|
||||
|
||||
def self.nipponese = Locale.find('ja')
|
||||
|
||||
private
|
||||
|
||||
def generate_tag_names!
|
||||
ApplicationRecord.transaction do
|
||||
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.tag_name('ja')),
|
||||
script_code:,
|
||||
primary_flg: true,
|
||||
auto_generated: true,
|
||||
canonical_id: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -126,13 +126,14 @@ class Post < ApplicationRecord
|
||||
super(options).merge(thumbnail: nil)
|
||||
end
|
||||
|
||||
def snapshot_tag_names
|
||||
def snapshot_tag_names language_code = 'ja'
|
||||
post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.joins(tag: :tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.order('tag_names.name')
|
||||
.map do |post_tag|
|
||||
name = post_tag.tag.tag_name.name
|
||||
name = post_tag.tag.name(language_code)
|
||||
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||
|
||||
next name if sections.empty?
|
||||
@@ -152,16 +153,17 @@ class Post < ApplicationRecord
|
||||
"#{ tag.fetch('name') }#{ sections.join }"
|
||||
end
|
||||
|
||||
def snapshot_tags_json
|
||||
def snapshot_tags_json language_code = 'ja'
|
||||
tag_snapshots =
|
||||
post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.joins(tag: :tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.order('tags.id')
|
||||
.map { |pt|
|
||||
{ 'tag_id' => pt.tag.id,
|
||||
'version_no' => pt.tag.version_no,
|
||||
'name' => pt.tag.name,
|
||||
'name' => pt.tag.name(language_code),
|
||||
'category' => pt.tag.category,
|
||||
'sections' => pt.sections.sort_by(&:begin_ms).map {
|
||||
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
class Region < ApplicationRecord
|
||||
;
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class Script < ApplicationRecord
|
||||
has_many :locales, foreign_key: :script_code, primary_key: :code
|
||||
|
||||
enum :code, Jpan: 'Jpan'
|
||||
end
|
||||
+286
-234
@@ -50,14 +50,10 @@ class Tag < ApplicationRecord
|
||||
|
||||
has_many :tag_versions
|
||||
|
||||
belongs_to :tag_name
|
||||
delegate :wiki_page, to: :tag_name
|
||||
has_many :tag_names
|
||||
|
||||
attribute :version_no, :integer, default: 1
|
||||
|
||||
delegate :name, to: :tag_name, allow_nil: true
|
||||
validates :tag_name, presence: true
|
||||
|
||||
enum :category, deerjikist: 'deerjikist',
|
||||
meme: 'meme',
|
||||
character: 'character',
|
||||
@@ -67,9 +63,8 @@ class Tag < ApplicationRecord
|
||||
|
||||
validates :category, presence: true, inclusion: { in: Tag.categories.keys }
|
||||
|
||||
validate :tag_name_mustnt_start_with_nico
|
||||
validate :tag_name_must_be_canonical
|
||||
validate :category_must_be_deerjikist_with_deerjikists
|
||||
validate :tag_names_must_be_valid
|
||||
|
||||
CATEGORY_PREFIXES = {
|
||||
'general:' => :general,
|
||||
@@ -83,8 +78,20 @@ class Tag < ApplicationRecord
|
||||
'mtr:' => :material,
|
||||
'meta:' => :meta }.freeze
|
||||
|
||||
def name= val
|
||||
(self.tag_name ||= build_tag_name).name = val
|
||||
SYSTEM_TAG_NAMES = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ', 'YouTube'].freeze
|
||||
|
||||
def tag_name language_code
|
||||
if tag_names.loaded?
|
||||
tag_names.find { _1.language_code == language_code && _1.primary_flg }
|
||||
else
|
||||
tag_names.find_by(language_code:, primary_flg: true)
|
||||
end
|
||||
end
|
||||
def name(language_code) = tag_name(language_code)&.name
|
||||
def wiki_page = tag_name('ja')&.wiki_page
|
||||
|
||||
def system?
|
||||
tag_name('ja')&.name.in?(SYSTEM_TAG_NAMES)
|
||||
end
|
||||
|
||||
def deprecated? = deprecated_at?
|
||||
@@ -95,214 +102,285 @@ class Tag < ApplicationRecord
|
||||
|
||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||
|
||||
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
|
||||
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
|
||||
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
|
||||
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
|
||||
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
|
||||
def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
|
||||
|
||||
def self.normalise_tags! tag_names, with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
end
|
||||
|
||||
sections = { }
|
||||
tags = tag_names.map do |name|
|
||||
raw_name = name
|
||||
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil]
|
||||
|
||||
name = name.sub(/\A#{ pf }/i, '')
|
||||
|
||||
sections_by_tag = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
sections_by_tag << normalise_section_range!(
|
||||
begin_raw: match[2],
|
||||
end_raw: match[3],
|
||||
tag_name: name)
|
||||
end
|
||||
|
||||
if name.include?('[') || name.include?(']')
|
||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
name = TagName.canonicalise(name).first
|
||||
|
||||
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
|
||||
if deny_deprecated && tag.deprecated?
|
||||
raise DeprecatedTagNormalisationError, [tag.name]
|
||||
end
|
||||
|
||||
tag.update!(category: cat) if cat && tag.category != cat
|
||||
next if sections_by_tag.empty?
|
||||
|
||||
sections[tag.id] ||= []
|
||||
sections[tag.id].concat(sections_by_tag)
|
||||
sections[tag.id] = merge_section_ranges(sections[tag.id])
|
||||
sections.delete(tag.id) if sections[tag.id] == [[0, nil]]
|
||||
end
|
||||
end
|
||||
|
||||
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||
tags << Tag.no_deerjikist if with_no_deerjikist && tags.all? { |t| !(t.deerjikist?) }
|
||||
tags.uniq!(&:id)
|
||||
|
||||
if with_sections
|
||||
{ tags:, sections: }
|
||||
else
|
||||
tags
|
||||
end
|
||||
def snapshot_aliases(language_code)
|
||||
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
|
||||
end
|
||||
|
||||
def self.expand_parent_tags tags
|
||||
return [] if tags.blank?
|
||||
|
||||
seen = Set.new
|
||||
result = []
|
||||
stack = tags.compact.dup
|
||||
|
||||
until stack.empty?
|
||||
tag = stack.pop
|
||||
next unless tag
|
||||
|
||||
tag.parents.each do |parent|
|
||||
next if seen.include?(parent.id)
|
||||
|
||||
seen << parent.id
|
||||
result << parent
|
||||
stack << parent
|
||||
end
|
||||
end
|
||||
|
||||
(result + tags).uniq { |t| t.id }
|
||||
end
|
||||
|
||||
def self.normalise_section_range! begin_raw:, end_raw:, tag_name:
|
||||
begin_ms = begin_raw.empty? ? 0 : time_to_ms!(begin_raw, tag_name:)
|
||||
end_ms = end_raw.empty? ? nil : time_to_ms!(end_raw, tag_name:)
|
||||
|
||||
if end_ms
|
||||
begin_ms, end_ms = end_ms, begin_ms if begin_ms > end_ms
|
||||
end_ms = begin_ms + 1 if begin_ms == end_ms
|
||||
end
|
||||
|
||||
[begin_ms, end_ms]
|
||||
end
|
||||
|
||||
def self.merge_section_ranges ranges
|
||||
sorted_ranges = ranges.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
|
||||
merged = []
|
||||
|
||||
sorted_ranges.each do |begin_ms, end_ms|
|
||||
if merged.empty?
|
||||
merged << [begin_ms, end_ms]
|
||||
next
|
||||
end
|
||||
|
||||
last_begin_ms, last_end_ms = merged[-1]
|
||||
if last_end_ms.nil? || begin_ms <= last_end_ms
|
||||
merged[-1] = [last_begin_ms, merge_section_end(last_end_ms, end_ms)]
|
||||
else
|
||||
merged << [begin_ms, end_ms]
|
||||
end
|
||||
end
|
||||
|
||||
merged
|
||||
end
|
||||
|
||||
def self.merge_section_end left_end_ms, right_end_ms
|
||||
return nil if left_end_ms.nil? || right_end_ms.nil?
|
||||
|
||||
[left_end_ms, right_end_ms].max
|
||||
end
|
||||
|
||||
def self.find_or_create_by_tag_name! name, category:
|
||||
tn = TagName.find_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|
|
||||
t.category = category
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
|
||||
def self.merge_tags! target_tag, source_tags, created_by_user: nil
|
||||
target_tag => Tag
|
||||
|
||||
affected_post_ids = Set.new
|
||||
|
||||
Tag.transaction do
|
||||
TagVersioning.ensure_snapshot!(target_tag, created_by_user:)
|
||||
|
||||
Array(source_tags).compact.uniq.each do |source_tag|
|
||||
source_tag => Tag
|
||||
|
||||
next if source_tag == target_tag
|
||||
|
||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user:)
|
||||
|
||||
source_tag.post_tags.find_each do |source_pt|
|
||||
post_id = source_pt.post_id
|
||||
affected_post_ids << post_id
|
||||
source_pt.destroy!
|
||||
unless PostTag.exists?(post_id:, tag: target_tag)
|
||||
PostTag.create!(post_id:, tag: target_tag)
|
||||
end
|
||||
end
|
||||
|
||||
source_tag_name = source_tag.tag_name
|
||||
|
||||
if source_tag_name.wiki_page.present?
|
||||
raise ActiveRecord::RecordInvalid.new(source_tag_name)
|
||||
end
|
||||
|
||||
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
|
||||
source_tag.destroy!
|
||||
|
||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||
updated_at: Time.current)
|
||||
|
||||
TagVersioning.record!(target_tag, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
Post.where(id: affected_post_ids.to_a).find_each do |post|
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user:)
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
# 投稿件数を再集計
|
||||
target_tag.update_columns(post_count: PostTag.where(tag: target_tag).count)
|
||||
end
|
||||
|
||||
target_tag.reload
|
||||
end
|
||||
|
||||
def snapshot_aliases = tag_name.aliases.order(:name).pluck(:name)
|
||||
|
||||
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
||||
|
||||
def snapshot_linked_tag_names
|
||||
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
def snapshot_linked_tag_names language_code
|
||||
linked_tags
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.order('tag_names.name')
|
||||
.pluck('tag_names.name')
|
||||
end
|
||||
|
||||
class << self
|
||||
def tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta)
|
||||
def bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
|
||||
def no_deerjikist =
|
||||
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
|
||||
def video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
|
||||
def niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
|
||||
def youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
|
||||
|
||||
def normalise_tags! locale, tag_names,
|
||||
with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
end
|
||||
|
||||
sections = { }
|
||||
tags = tag_names.map do |name|
|
||||
raw_name = name
|
||||
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil]
|
||||
|
||||
name = name.sub(/\A#{ pf }/i, '')
|
||||
|
||||
sections_by_tag = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
sections_by_tag << normalise_section_range!(
|
||||
begin_raw: match[2],
|
||||
end_raw: match[3],
|
||||
tag_name: name)
|
||||
end
|
||||
|
||||
if name.include?('[') || name.include?(']')
|
||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
name = TagName.canonicalise(locale, name).first
|
||||
|
||||
find_or_create_by_tag_name!(locale, name, category: (cat || :general)).tap do |tag|
|
||||
if deny_deprecated && tag.deprecated?
|
||||
raise DeprecatedTagNormalisationError, [tag.name(locale.language_code)]
|
||||
end
|
||||
|
||||
tag.update!(category: cat) if cat && tag.category != cat
|
||||
next if sections_by_tag.empty?
|
||||
|
||||
sections[tag.id] ||= []
|
||||
sections[tag.id].concat(sections_by_tag)
|
||||
sections[tag.id] = merge_section_ranges(sections[tag.id])
|
||||
sections.delete(tag.id) if sections[tag.id] == [[0, nil]]
|
||||
end
|
||||
end
|
||||
|
||||
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||
tags << Tag.no_deerjikist if with_no_deerjikist && tags.all? { |t| !(t.deerjikist?) }
|
||||
tags.uniq!(&:id)
|
||||
|
||||
if with_sections
|
||||
{ tags:, sections: }
|
||||
else
|
||||
tags
|
||||
end
|
||||
end
|
||||
|
||||
def expand_parent_tags tags
|
||||
return [] if tags.blank?
|
||||
|
||||
seen = Set.new
|
||||
result = []
|
||||
stack = tags.compact.dup
|
||||
|
||||
until stack.empty?
|
||||
tag = stack.pop
|
||||
next unless tag
|
||||
|
||||
tag.parents.each do |parent|
|
||||
next if seen.include?(parent.id)
|
||||
|
||||
seen << parent.id
|
||||
result << parent
|
||||
stack << parent
|
||||
end
|
||||
end
|
||||
|
||||
(result + tags).uniq { |t| t.id }
|
||||
end
|
||||
|
||||
def normalise_section_range! begin_raw:, end_raw:, tag_name:
|
||||
begin_ms = begin_raw.empty? ? 0 : time_to_ms!(begin_raw, tag_name:)
|
||||
end_ms = end_raw.empty? ? nil : time_to_ms!(end_raw, tag_name:)
|
||||
|
||||
if end_ms
|
||||
begin_ms, end_ms = end_ms, begin_ms if begin_ms > end_ms
|
||||
end_ms = begin_ms + 1 if begin_ms == end_ms
|
||||
end
|
||||
|
||||
[begin_ms, end_ms]
|
||||
end
|
||||
|
||||
def merge_section_ranges ranges
|
||||
sorted_ranges = ranges.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
|
||||
merged = []
|
||||
|
||||
sorted_ranges.each do |begin_ms, end_ms|
|
||||
if merged.empty?
|
||||
merged << [begin_ms, end_ms]
|
||||
next
|
||||
end
|
||||
|
||||
last_begin_ms, last_end_ms = merged[-1]
|
||||
if last_end_ms.nil? || begin_ms <= last_end_ms
|
||||
merged[-1] = [last_begin_ms, merge_section_end(last_end_ms, end_ms)]
|
||||
else
|
||||
merged << [begin_ms, end_ms]
|
||||
end
|
||||
end
|
||||
|
||||
merged
|
||||
end
|
||||
|
||||
def merge_section_end left_end_ms, right_end_ms
|
||||
return nil if left_end_ms.nil? || right_end_ms.nil?
|
||||
|
||||
[left_end_ms, right_end_ms].max
|
||||
end
|
||||
|
||||
def find_or_create_by_tag_name! locale, name, category:
|
||||
language_code = locale.language_code
|
||||
name = name.to_s.strip
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
tn = TagName.find_or_create_by!(language_code:, name:) do
|
||||
_1.script_code = locale.script_code
|
||||
_1.primary_flg = true
|
||||
end
|
||||
|
||||
tag = tn.tag
|
||||
next tag if tag
|
||||
|
||||
tag = Tag.create!(tag_name_id: tn.id, category:)
|
||||
tn.update!(tag:, primary_flg: true, canonical_id: nil, auto_generated: false)
|
||||
generate_tag_names!(locale, tag, tn)
|
||||
tag.association(:tag_names).reset
|
||||
|
||||
tag
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
|
||||
def merge_tags! target_tag, source_tags, created_by_user: nil
|
||||
target_tag => Tag
|
||||
|
||||
affected_post_ids = Set.new
|
||||
|
||||
Tag.transaction do
|
||||
TagVersioning.ensure_snapshot!(target_tag, created_by_user:)
|
||||
|
||||
Array(source_tags).compact.uniq.each do |source_tag|
|
||||
source_tag => Tag
|
||||
|
||||
next if source_tag == target_tag
|
||||
|
||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user:)
|
||||
|
||||
source_tag.post_tags.find_each do |source_pt|
|
||||
post_id = source_pt.post_id
|
||||
affected_post_ids << post_id
|
||||
source_pt.destroy!
|
||||
unless PostTag.exists?(post_id:, tag: target_tag)
|
||||
PostTag.create!(post_id:, tag: target_tag)
|
||||
end
|
||||
end
|
||||
|
||||
source_tag_name = source_tag.tag_name('ja')
|
||||
|
||||
if source_tag_name.wiki_page.present?
|
||||
raise ActiveRecord::RecordInvalid.new(source_tag_name)
|
||||
end
|
||||
|
||||
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
|
||||
source_tag.tag_names.update_all(tag_id: target_tag.id, primary_flg: false,
|
||||
updated_at: Time.current)
|
||||
source_tag.destroy!
|
||||
|
||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||
updated_at: Time.current)
|
||||
|
||||
TagVersioning.record!(target_tag, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
Post.where(id: affected_post_ids.to_a).find_each do |post|
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user:)
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
# 投稿件数を再集計
|
||||
target_tag.update_columns(post_count: PostTag.where(tag: target_tag).count)
|
||||
end
|
||||
|
||||
target_tag.reload
|
||||
end
|
||||
|
||||
def time_to_ms! str, tag_name:
|
||||
match =
|
||||
case str
|
||||
when /\A(?<seconds>\d+)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: nil, seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<minutes>\d+):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<hours>\d+):(?<minutes>[0-5]?\d):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: Regexp.last_match[:hours],
|
||||
minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
end
|
||||
|
||||
raise SectionLiteralParseError.new(tag_name, str) unless match
|
||||
|
||||
total_s = match[:seconds].to_i
|
||||
total_s += match[:minutes].to_i * 60 if match[:minutes]
|
||||
total_s += match[:hours].to_i * 3_600 if match[:hours]
|
||||
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_tag_names! locale, tag, base_tag_name
|
||||
ApplicationRecord.transaction do
|
||||
locales =
|
||||
Locale
|
||||
.where(deprecated_at: nil)
|
||||
.where.not(language_code: locale.language_code)
|
||||
.order(:language_code, :created_at, :script_code)
|
||||
.uniq { _1.language_code }
|
||||
locales.each do |locale|
|
||||
language_code = locale.language_code
|
||||
script_code = locale.script_code
|
||||
TagName.create!(tag_id: tag.id,
|
||||
language_code:,
|
||||
name: TagName.generate_name(locale, tag, base_tag_name),
|
||||
script_code:,
|
||||
primary_flg: true,
|
||||
auto_generated: true,
|
||||
canonical_id: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_name_mustnt_start_with_nico
|
||||
if name.to_s.downcase.start_with?('nico:')
|
||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
def tag_names_must_be_valid
|
||||
tag_names.each do |tag_name|
|
||||
next if tag_name.valid?
|
||||
|
||||
def tag_name_must_be_canonical
|
||||
if tag_name&.canonical_id?
|
||||
errors.add :tag_name, 'tag_names へは実体を示す必要があります.'
|
||||
tag_name.errors[:name].each { errors.add :name, _1 }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -311,30 +389,4 @@ class Tag < ApplicationRecord
|
||||
errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.'
|
||||
end
|
||||
end
|
||||
|
||||
def self.time_to_ms! str, tag_name:
|
||||
match =
|
||||
case str
|
||||
when /\A(?<seconds>\d+)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: nil, seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<minutes>\d+):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<hours>\d+):(?<minutes>[0-5]?\d):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: Regexp.last_match[:hours],
|
||||
minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
end
|
||||
|
||||
raise SectionLiteralParseError.new(tag_name, str) unless match
|
||||
|
||||
total_s = match[:seconds].to_i
|
||||
total_s += match[:minutes].to_i * 60 if match[:minutes]
|
||||
total_s += match[:hours].to_i * 3_600 if match[:hours]
|
||||
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,43 +1,61 @@
|
||||
class TagName < ApplicationRecord
|
||||
has_one :tag
|
||||
belongs_to :language, foreign_key: :language_code, primary_key: :code
|
||||
belongs_to :tag, optional: true
|
||||
has_one :wiki_page
|
||||
|
||||
belongs_to :canonical, class_name: 'TagName', optional: true
|
||||
has_many :aliases, class_name: 'TagName', foreign_key: :canonical_id
|
||||
validates :name, presence: true,
|
||||
length: { maximum: 255 },
|
||||
uniqueness: { scope: :language_code }
|
||||
|
||||
validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
|
||||
|
||||
validate :canonical_must_be_canonical
|
||||
validate :alias_name_must_not_have_prefix
|
||||
validate :canonical_must_not_be_present_with_tag_or_wiki_page
|
||||
validate :alias_must_not_have_wiki_page
|
||||
validate :name_must_be_sanitised
|
||||
validate :name_mustnt_start_with_nico
|
||||
validate :generated_name_must_be_reserved
|
||||
|
||||
def self.canonicalise names
|
||||
def primary? = primary_flg
|
||||
|
||||
def self.canonicalise locale, names
|
||||
names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?)
|
||||
return [] if names.blank?
|
||||
|
||||
tns = TagName.includes(:canonical).where(name: names).index_by(&:name)
|
||||
tns = TagName.where(language_code: locale.language_code, name: names).index_by(&:name)
|
||||
|
||||
names.map { |name| tns[name]&.canonical&.name || name }.uniq
|
||||
names.map { |name|
|
||||
tag_name = tns[name]
|
||||
if !tag_name || tag_name.primary? || tag_name.tag_id.nil?
|
||||
name
|
||||
else
|
||||
TagName.find_by(language_code: locale.language_code,
|
||||
tag_id: tag_name.tag_id,
|
||||
primary_flg: true)&.name || name
|
||||
end
|
||||
}.uniq
|
||||
end
|
||||
|
||||
def self.generate_name locale, tag, base_tag_name
|
||||
# TODO: 言語ごとの自動命名ロジック完成したら書く.
|
||||
|
||||
"Tag_##{ tag.id }"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def canonical_must_be_canonical
|
||||
if canonical&.canonical_id?
|
||||
errors.add :canonical, 'canonical は実体を示す必要があります.'
|
||||
def generated_name_must_be_reserved
|
||||
if !auto_generated && name.to_s.start_with?('Tag_#')
|
||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
|
||||
def alias_name_must_not_have_prefix
|
||||
if canonical_id? && name.to_s.include?(':')
|
||||
if !(primary?) && name.to_s.include?(':')
|
||||
errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.'
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_must_not_be_present_with_tag_or_wiki_page
|
||||
if canonical_id? && (tag || wiki_page)
|
||||
errors.add :canonical, 'タグもしくは Wiki の参照がある名前はエーリアスになれません.'
|
||||
def alias_must_not_have_wiki_page
|
||||
if !(primary?) && wiki_page
|
||||
errors.add :primary_flg, 'Wiki 参照がある名前はエーリアスになれません.'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,4 +64,10 @@ class TagName < ApplicationRecord
|
||||
errors.add :name, '名前に使用できない文字が含まれてゐます.'
|
||||
end
|
||||
end
|
||||
|
||||
def name_mustnt_start_with_nico
|
||||
if name.to_s.downcase.start_with?('nico:')
|
||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,31 +15,39 @@ class TagNameSanitisationRule < ApplicationRecord
|
||||
end
|
||||
|
||||
def apply!
|
||||
TagName.find_each do |tn|
|
||||
name = sanitise(tn.name)
|
||||
next if name == tn.name
|
||||
Language.find_each do |language|
|
||||
TagName.where(language:).find_each do |tn|
|
||||
name = sanitise(tn.name)
|
||||
next if name == tn.name
|
||||
|
||||
TagName.transaction do
|
||||
existing_tn = TagName.find_by(name:)
|
||||
if existing_tn
|
||||
existing_tn = existing_tn.canonical || existing_tn
|
||||
next if existing_tn.id == tn.id
|
||||
TagName.transaction do
|
||||
existing_tn = TagName.find_by(language:, name:)
|
||||
if existing_tn
|
||||
unless existing_tn.primary_flg
|
||||
existing_tn = TagName.find_by!(language:,
|
||||
tag_id: existing_tn.tag_id,
|
||||
primary_flg: true)
|
||||
end
|
||||
next if existing_tn.id == tn.id
|
||||
|
||||
existing_tag = Tag.find_by(tag_name_id: existing_tn.id)
|
||||
source_tag = Tag.find_by(tag_name_id: tn.id)
|
||||
existing_tag = existing_tn.tag
|
||||
source_tag = tn.tag
|
||||
|
||||
if existing_tag
|
||||
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
|
||||
elsif source_tag
|
||||
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
|
||||
if existing_tag
|
||||
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
|
||||
elsif source_tag
|
||||
tn.update_columns(tag_id: nil, updated_at: Time.current)
|
||||
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
|
||||
existing_tn.update_columns(tag_id: source_tag.id, updated_at: Time.current)
|
||||
end
|
||||
tn.destroy!
|
||||
|
||||
next
|
||||
end
|
||||
tn.destroy!
|
||||
|
||||
next
|
||||
# TagName 側の自動サニタイズを回避
|
||||
tn.update_columns(name:, updated_at: Time.current)
|
||||
end
|
||||
|
||||
# TagName 側の自動サニタイズを回避
|
||||
tn.update_columns(name:, updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,5 +11,6 @@ class TagVersion < ApplicationRecord
|
||||
meta: 'meta' }, validate: true
|
||||
|
||||
validates :name, presence: true
|
||||
validates :names, presence: true
|
||||
validates :category, presence: true
|
||||
end
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする