コミットを比較

..
103 コミット
作成者 SHA1 メッセージ 日付
みてるぞ 46e6d94edb #399 2026-07-18 23:38:16 +09:00
みてるぞ 817cd6ae73 #399 2026-07-18 23:06:12 +09:00
みてるぞ dc7353304f #399 2026-07-18 22:53:36 +09:00
みてるぞ 5ff3fc9441 #399 2026-07-18 22:32:34 +09:00
みてるぞ a847d93d2f #399 2026-07-18 22:18:20 +09:00
みてるぞ 2f87669699 #399 2026-07-18 21:59:22 +09:00
みてるぞ 1c906d7432 #399 2026-07-18 21:19:53 +09:00
みてるぞ f662dc9dc0 #399 2026-07-18 20:22:53 +09:00
みてるぞ 206c6bc0a0 #399 2026-07-18 20:10:15 +09:00
みてるぞ 8f66ee8059 #399 2026-07-18 20:03:26 +09:00
みてるぞ 83e3db3314 #399 2026-07-18 20:00:40 +09:00
みてるぞ eae6c30064 #399 2026-07-18 19:26:27 +09:00
みてるぞ 240e078f0b #399 2026-07-18 18:33:11 +09:00
みてるぞ 74b1ada0dd #399 2026-07-18 17:44:56 +09:00
みてるぞ 23d8adf65d #399 2026-07-18 17:35:28 +09:00
みてるぞ e0debed94e #399 2026-07-18 17:31:57 +09:00
みてるぞ 583ce22a7e #399 2026-07-18 17:07:56 +09:00
みてるぞ 7c808a6f76 #399 2026-07-18 15:15:10 +09:00
みてるぞ a5ae7c6f2d #399 2026-07-18 14:55:11 +09:00
みてるぞ 03dc4d0661 #399 2026-07-18 13:32:22 +09:00
みてるぞ 06b9c1cb50 #399 2026-07-18 13:07:49 +09:00
みてるぞ 688b4af575 #399 2026-07-18 12:53:50 +09:00
みてるぞ 040cc3f25d #399 2026-07-18 12:15:32 +09:00
みてるぞ b3e67d8cca #399 2026-07-18 11:55:09 +09:00
みてるぞ b00a62a0ac #399 2026-07-18 09:11:11 +09:00
みてるぞ e6c3c635b8 #399 2026-07-18 02:29:55 +09:00
みてるぞ 9552081133 #399 2026-07-18 02:08:20 +09:00
みてるぞ 2d2a9b4bd6 #399 2026-07-18 01:37:11 +09:00
みてるぞ 09ac2576bb #399 2026-07-18 01:28:22 +09:00
みてるぞ 9eca670934 #399 2026-07-18 01:12:55 +09:00
みてるぞ c31d84115d #399 2026-07-18 00:31:02 +09:00
みてるぞ ff970f8171 #399 2026-07-18 00:01:12 +09:00
みてるぞ 0ae41b6266 #399 2026-07-17 23:14:37 +09:00
みてるぞ eb4bf5e35c #399 2026-07-17 22:21:18 +09:00
みてるぞ e6b7e33b83 #399 2026-07-17 21:53:24 +09:00
みてるぞ 3820d3d4d5 #399 2026-07-17 21:06:26 +09:00
みてるぞ dd2d199d04 #399 2026-07-16 23:39:13 +09:00
みてるぞ 0a8ffc38b8 #399 2026-07-16 23:23:32 +09:00
みてるぞ 3f75994bd4 #399 2026-07-16 23:12:14 +09:00
みてるぞ f91b78bd47 #399 2026-07-16 22:31:32 +09:00
みてるぞ 7f8cce39bc #399 2026-07-16 22:05:40 +09:00
みてるぞ 5c5a9fa1b0 #399 2026-07-16 22:00:16 +09:00
みてるぞ 66c738cbff #399 2026-07-16 21:45:02 +09:00
みてるぞ cde0a2deae #399 2026-07-16 20:54:42 +09:00
みてるぞ 43a3772976 #399 2026-07-16 20:35:07 +09:00
みてるぞ 90d8d3ff08 #399 2026-07-16 20:05:09 +09:00
みてるぞ d1de631eed #399 2026-07-16 19:35:21 +09:00
みてるぞ 6f3ccb0ffa #399 2026-07-16 19:18:35 +09:00
みてるぞ 95c3f08a44 #399 2026-07-16 18:52:07 +09:00
みてるぞ ef95b20a7e #399 2026-07-16 18:33:45 +09:00
みてるぞ f76fbe6711 #399 2026-07-16 18:16:20 +09:00
みてるぞ 0224c4d2f4 #399 2026-07-16 12:39:35 +09:00
みてるぞ ead825e3ee #399 2026-07-16 07:56:45 +09:00
みてるぞ 3d88fdecb1 #399 2026-07-16 01:19:36 +09:00
みてるぞ 2e1b4449ba #399 2026-07-16 00:44:11 +09:00
みてるぞ 4eb483c049 #399 2026-07-16 00:24:24 +09:00
みてるぞ e197f27e9a #399 2026-07-15 23:59:01 +09:00
みてるぞ 34f81325d2 #399 2026-07-15 23:42:05 +09:00
みてるぞ 9af4670c6c #399 2026-07-15 23:29:19 +09:00
みてるぞ 5183b0b354 #399 2026-07-15 23:06:28 +09:00
みてるぞ d0914fce3b #399 2026-07-15 22:51:01 +09:00
みてるぞ 0ac7332458 #399 2026-07-15 22:41:06 +09:00
みてるぞ 07ce19e32d #399 2026-07-15 22:26:58 +09:00
みてるぞ 5f1d619139 #399 2026-07-15 22:04:47 +09:00
みてるぞ 3cd22855b3 #399 2026-07-15 21:42:00 +09:00
みてるぞ 55c69866a7 #399 2026-07-15 21:37:03 +09:00
みてるぞ 04d2f0f5a9 #399 2026-07-15 20:59:00 +09:00
みてるぞ 025f49cbcb #399 2026-07-15 20:51:24 +09:00
みてるぞ d2f1b1ed48 #399 2026-07-15 20:25:51 +09:00
みてるぞ 6e5aa1e30f #399 2026-07-15 19:57:09 +09:00
みてるぞ 21d6c1408a #399 2026-07-15 19:43:39 +09:00
みてるぞ eebbd5d069 #399 2026-07-15 19:33:18 +09:00
みてるぞ 58828597d7 #399 2026-07-15 08:03:25 +09:00
みてるぞ b0c24f319a #399 2026-07-14 20:03:11 +09:00
みてるぞ f9463f383f #399 2026-07-14 19:37:04 +09:00
みてるぞ 6d037192c4 #399 2026-07-14 19:12:23 +09:00
みてるぞ d035da99ad #399 2026-07-14 18:39:24 +09:00
みてるぞ bc660676ef Merge remote-tracking branch 'origin/main' into feature/399 2026-07-14 12:40:32 +09:00
みてるぞ 97b132e5ab #399 2026-07-13 01:31:45 +09:00
みてるぞ be68841bd3 #399 2026-07-13 01:17:11 +09:00
みてるぞ ac82adc6b3 #399 2026-07-13 00:58:02 +09:00
みてるぞ aa96ec95d1 #399 2026-07-12 14:03:05 +09:00
みてるぞ df17f20907 #399 2026-07-12 13:14:36 +09:00
みてるぞ 769966648b #399 2026-07-12 12:43:31 +09:00
みてるぞ 7bcb76516c #399 2026-07-12 12:23:53 +09:00
みてるぞ 8970edc59f #399 2026-07-12 11:17:23 +09:00
みてるぞ 9b1ea56e36 #399 2026-07-12 10:27:13 +09:00
みてるぞ 155edfe018 #399 2026-07-12 02:22:36 +09:00
みてるぞ 23f1ffb04b #399 2026-07-12 02:08:32 +09:00
みてるぞ ccfe65a6a4 #399 2026-07-12 02:01:25 +09:00
みてるぞ 06d6e512e4 #399 2026-07-12 01:56:24 +09:00
みてるぞ 5bd097bcfe #399 2026-07-12 01:47:34 +09:00
みてるぞ f636d2a177 #399 2026-07-12 01:39:31 +09:00
みてるぞ 10dc776313 #399 2026-07-12 01:17:45 +09:00
みてるぞ a9e16735f8 #399 2026-07-12 01:11:07 +09:00
みてるぞ 440a6c9961 #399 2026-07-12 00:54:21 +09:00
みてるぞ 4535a9d260 #399 2026-07-12 00:46:53 +09:00
みてるぞ 440d3d38be #399 2026-07-11 23:21:59 +09:00
みてるぞ cb33d9ca25 #399 2026-07-11 23:08:03 +09:00
みてるぞ 19bf24432a #399 2026-07-11 23:07:51 +09:00
みてるぞ ac41385962 Merge remote-tracking branch 'origin/main' into feature/399 2026-07-11 22:36:03 +09:00
みてるぞ 08bf92ff79 #399 2026-07-11 21:46:10 +09:00
みてるぞ c51d7b98ba #399 2026-07-11 21:11:36 +09:00
23個のファイルの変更129行の追加828行の削除
-7
ファイルの表示
@@ -85,11 +85,4 @@ class ApplicationController < ActionController::API
base_errors: },
status:
end
def normalise_json value
return nil if value.nil?
return JSON.parse(value) if value.is_a?(String)
value
end
end
+4 -4
ファイルの表示
@@ -236,10 +236,10 @@ class MaterialsController < ApplicationController
nil
end
def resolve_material_tag! locale, tag_name_raw
tag_name = TagName.find_undiscard_or_create_by!(language_code: locale.language_code,
name: tag_name_raw)
tag_name.tag || Tag.create!(tag_name:, category: :material)
def resolve_material_tag! 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
def material_index_needs_tag_name? filters
+55 -64
ファイルの表示
@@ -1,7 +1,7 @@
class PostVersionsController < ApplicationController
def index
post_id = params[:post].presence
tag_id = params[:tag].presence&.to_i
tag_id = params[:tag].presence
page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i
@@ -10,6 +10,12 @@ class PostVersionsController < ApplicationController
offset = (page - 1) * limit
tag_name =
if tag_id
TagName.joins(:tag).find_by(tag: { id: tag_id })
end
return render json: { versions: [], count: 0 } if tag_id && tag_name.blank?
q = PostVersion.joins(<<~SQL.squish)
LEFT JOIN
post_versions prev
@@ -17,18 +23,17 @@ class PostVersionsController < ApplicationController
prev.post_id = post_versions.post_id
AND prev.version_no = post_versions.version_no - 1
SQL
.select('post_versions.*',
'prev.title AS prev_title',
'prev.url AS prev_url',
'prev.thumbnail_base AS prev_thumbnail_base',
'prev.tags_json AS prev_tags_json',
.select('post_versions.*', 'prev.title AS prev_title', 'prev.url AS prev_url',
'prev.thumbnail_base AS prev_thumbnail_base', 'prev.tags AS prev_tags',
'prev.video_ms AS prev_video_ms',
'prev.original_created_from AS prev_original_created_from',
'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('id', #{ tag_id })) " +
"OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('id', #{ tag_id }))")
if tag_name
escaped = ActiveRecord::Base.sanitize_sql_like(tag_name.name)
q = q.where(("CONCAT(' ', post_versions.tags, ' ') LIKE :kw " +
"OR CONCAT(' ', prev.tags, ' ') LIKE :kw"),
kw: "% #{ escaped } %")
end
count = q.except(:select, :order, :limit, :offset).count
@@ -47,69 +52,51 @@ class PostVersionsController < ApplicationController
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
rows.map do |row|
cur_tags =
normalise_json(row.tags_json)
.sort_by { [(case _1.fetch('category')
when 'deerjikist'
0
when 'meme'
1
when 'character'
2
when 'general'
3
when 'material'
4
when 'meta'
5
else
6
end),
_1.fetch('name').downcase] }
.map { Post.tag_snapshot_literal(_1) }
prev_tags =
(normalise_json(row.attributes['prev_tags_json']) || [])
.sort_by { [(case _1.fetch('category')
when 'deerjikist'
0
when 'meme'
1
when 'character'
2
when 'general'
3
when 'material'
4
when 'meta'
5
else
6
end),
_1.fetch('name').downcase] }
.map { Post.tag_snapshot_literal(_1) }
cur_tags = split_tags(row.tags)
prev_tags = split_tags(row.attributes['prev_tags'])
{ post_id: row.post_id,
{
post_id: row.post_id,
version_no: row.version_no,
event_type: row.event_type,
title: { current: row.title, prev: row.attributes['prev_title'] },
url: { current: row.url, prev: row.attributes['prev_url'] },
thumbnail: { current: nil, prev: nil },
thumbnail_base: { current: row.thumbnail_base,
prev: row.attributes['prev_thumbnail_base'] },
video_ms: { current: row.video_ms, prev: row.attributes['prev_video_ms'] },
title: {
current: row.title,
prev: row.attributes['prev_title']
},
url: {
current: row.url,
prev: row.attributes['prev_url']
},
thumbnail: {
current: nil,
prev: nil
},
thumbnail_base: {
current: row.thumbnail_base,
prev: row.attributes['prev_thumbnail_base']
},
video_ms: {
current: row.video_ms,
prev: row.attributes['prev_video_ms']
},
tags: build_version_tags(cur_tags, prev_tags),
original_created_from: {
current: row.original_created_from&.iso8601,
prev: row.attributes['prev_original_created_from']&.iso8601 },
current: row.original_created_from&.iso8601,
prev: row.attributes['prev_original_created_from']&.iso8601
},
original_created_before: {
current: row.original_created_before&.iso8601,
prev: row.attributes['prev_original_created_before']&.iso8601 },
current: row.original_created_before&.iso8601,
prev: row.attributes['prev_original_created_before']&.iso8601
},
created_at: row.created_at.iso8601,
created_by_user:
if row.created_by_user_id
{ id: row.created_by_user_id,
name: users_by_id[row.created_by_user_id] }
end }
if row.created_by_user_id
{
id: row.created_by_user_id,
name: users_by_id[row.created_by_user_id]
}
end
}
end
end
@@ -130,4 +117,8 @@ class PostVersionsController < ApplicationController
}
end
end
def split_tags(tags)
tags.to_s.split(/\s+/).reject(&:blank?)
end
end
+11 -16
ファイルの表示
@@ -311,7 +311,6 @@ class PostsController < ApplicationController
base_version_no = parse_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
tag_names = params[:tags].to_s.split
original_created_from = params[:original_created_from]
@@ -333,8 +332,7 @@ class PostsController < ApplicationController
base_snapshot = post_snapshot_from_version(base_version)
current_snapshot = post_snapshot_from_record(post)
end
incoming_snapshot = post_incoming_snapshot(locale:,
title:,
incoming_snapshot = post_incoming_snapshot(title:,
original_created_from:,
original_created_before:,
tag_names:,
@@ -363,7 +361,7 @@ class PostsController < ApplicationController
end
end
apply_post_snapshot!(locale, post, snapshot_to_apply)
apply_post_snapshot!(post, snapshot_to_apply)
end
return render json: conflict_json, status: :conflict if conflict_json
@@ -701,10 +699,7 @@ class PostsController < ApplicationController
end
def editable_tag_names_from_version version
version.tags_json
.reject { _1.fetch('category') == 'nico' }
.map { Post.tag_snapshot_literal(_1) }
.sort
version.tags.to_s.split.reject { |name| name.downcase.start_with?('nico:') }.sort
end
def post_snapshot_from_record post
@@ -735,11 +730,11 @@ class PostsController < ApplicationController
end
end
def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:,
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
validate_original_created_values!(original_created_from, original_created_before)
Tag.normalise_tags!(locale, tag_names, with_tagme: false, deny_deprecated: true,
with_sections: true) =>
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
with_sections: true) =>
{ tags:, sections: }
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
@@ -875,7 +870,7 @@ class PostsController < ApplicationController
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
end
def apply_post_snapshot! locale, post, snapshot
def apply_post_snapshot! post, snapshot
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
post.update!(title: snapshot[:title],
@@ -883,10 +878,10 @@ class PostsController < ApplicationController
original_created_from: snapshot[:original_created_from],
original_created_before: snapshot[:original_created_before])
Tag.normalise_tags!(locale, snapshot[:tag_names],
with_tagme: false,
deny_deprecated: true,
with_sections: true) => { tags: editable_tags, sections: }
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
+7 -115
ファイルの表示
@@ -200,45 +200,14 @@ class TagsController < ApplicationController
.find_by(id: params[:id])
return head :not_found unless tag
rows = normalise_deerjikist_rows(tag)
return if performed?
ApplicationRecord.transaction do
tag.lock!
requested_keys = rows.map { |row| [row[:platform], row[:code]] }.uniq
row_indexes_by_key = rows_by_key(rows)
locked_deerjikists = lock_deerjikists_for_tag_update(tag.id, requested_keys)
current_deerjikists = locked_deerjikists.filter { |deerjikist|
deerjikist.tag_id == tag.id
}
requested_deerjikists = locked_deerjikists.filter { |deerjikist|
row_indexes_by_key.key?([deerjikist.platform, deerjikist.code])
}
render_deerjikist_conflicts(requested_deerjikists, row_indexes_by_key, tag)
raise ActiveRecord::Rollback if performed?
requested_keys_set = requested_keys.to_set
current_deerjikists.each do |deerjikist|
key = [deerjikist.platform, deerjikist.code]
deerjikist.destroy! unless requested_keys_set.include?(key)
end
existing_keys = requested_deerjikists.to_h { |deerjikist|
[[deerjikist.platform, deerjikist.code], true]
}
requested_keys.each do |platform, code|
next if existing_keys[[platform, code]]
deerjikist = Deerjikist.new(platform:, code:, tag:)
row_index = row_indexes_by_key[[platform, code]].first
begin
render_deerjikist_form_record_invalid(deerjikist, row_index) unless deerjikist.save
rescue ActiveRecord::RecordNotUnique
conflicts = lock_deerjikists_for_tag_update(tag.id, [[platform, code]])
render_deerjikist_conflicts(conflicts, row_indexes_by_key, tag)
end
tag.deerjikists = []
params[:_json].each.with_index do |item, i|
platform = item[:platform]
code = normalise_deerjikist_code(platform, item[:code])
deerjikist = Deerjikist.find_or_initialize_by(platform:, code:)
deerjikist.tag = tag
render_deerjikist_form_record_invalid(deerjikist, i) unless deerjikist.save
raise ActiveRecord::Rollback if performed?
end
end
@@ -684,7 +653,6 @@ class TagsController < ApplicationController
end
def normalise_deerjikist_code platform, code
code = code.to_s
return code if platform != 'youtube' || code[0] != '@'
url = "https://www.youtube.com/#{ code }"
@@ -701,82 +669,6 @@ class TagsController < ApplicationController
nil
end
def normalise_deerjikist_rows tag
rows = []
params[:_json].each.with_index do |item, index|
platform = item[:platform]
unless Deerjikist.platforms.key?(platform)
render_deerjikist_platform_invalid(index)
return rows
end
code = normalise_deerjikist_code(platform, item[:code])
deerjikist = Deerjikist.new(platform:, code:, tag:)
unless deerjikist.valid?
render_deerjikist_form_record_invalid(deerjikist, index)
return rows
end
rows << { index:, platform:, code: }
end
rows
end
def lock_deerjikists_for_tag_update tag_id, keys
clauses = ['tag_id = ?']
values = [tag_id]
keys.each do |platform, code|
clauses << '(platform = ? AND code = ?)'
values << platform << code
end
Deerjikist
.where(clauses.join(' OR '), *values)
.order(:platform, :code)
.lock
.to_a
end
def render_deerjikist_conflicts deerjikists, row_indexes_by_key, tag
conflicts = deerjikists.filter { |deerjikist| deerjikist.tag_id != tag.id }
return if conflicts.empty?
tag_names_by_id = Tag
.joins(:tag_name)
.where(id: conflicts.map(&:tag_id).uniq)
.pluck('tags.id', 'tag_names.name')
.to_h
fields = { }
conflicts.each do |deerjikist|
message = "この情報は既に「#{ tag_names_by_id[deerjikist.tag_id] }」に紐づいてゐます."
row_indexes_by_key[[deerjikist.platform, deerjikist.code]].each do |index|
field = :"deerjikists.#{ index }.code"
fields[field] ||= []
fields[field] << message
end
end
render_validation_error fields:
end
def render_deerjikist_platform_invalid index
render_validation_error fields: {
:"deerjikists.#{ index }.platform" => ['値が不正です.'],
}
end
def rows_by_key rows
rows.each_with_object({ }) do |row, result|
key = [row[:platform], row[:code]]
result[key] ||= []
result[key] << row[:index]
end
end
def render_deerjikist_form_record_invalid deerjikist, index
fields = { }
-3
ファイルの表示
@@ -1,3 +0,0 @@
class Language < ApplicationRecord
has_many :languages, class_name: 'Locale', foreign_key: :language_code
end
-24
ファイルの表示
@@ -1,24 +0,0 @@
class Locale < ApplicationRecord
after_create_commit :generate_tag_names!
belongs_to :language, foreign_key: :language_code, primary_key: :code
belongs_to :script, foreign_key: :script_code, primary_key: :code
def self.nipponese = Locale.find('ja')
private
def generate_tag_names!
tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id)
Tag.where.not(id: tag_ids).find_each do
TagName.create!(tag_id: _1.id,
language_code:,
name: TagName.generate_name(self, _1, _1.name),
script_code:,
primary_flg: true,
# TODO: 公証実装したら書く.
# auto_generated: true,
canonical_id: nil)
end
end
end
-28
ファイルの表示
@@ -137,34 +137,6 @@ class Post < ApplicationRecord
end
end
def self.tag_snapshot_literal tag
sections = tag.fetch('sections', []).map do |sec|
begin_ms = sec.fetch('begin_ms')
end_ms = sec['end_ms']
"[#{ Post.ms_to_time(begin_ms) }-#{ end_ms ? Post.ms_to_time(end_ms) : '' }]"
end
"#{ tag.fetch('name') }#{ sections.join }"
end
def snapshot_tags_json
post_tags
.kept
.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
end_ms =
section.end_ms ? Post.ms_to_time(section.end_ms) : ''
-3
ファイルの表示
@@ -1,3 +0,0 @@
class Script < ApplicationRecord
;
end
+17 -30
ファイルの表示
@@ -56,7 +56,6 @@ class Tag < ApplicationRecord
has_many :tag_versions
has_many :nico_tag_versions
has_many :tag_names
belongs_to :tag_name
delegate :wiki_page, to: :tag_name
@@ -106,20 +105,18 @@ class Tag < ApplicationRecord
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
def self.tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', 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!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', 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.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! locale, tag_names,
with_tagme: true,
with_no_deerjikist: true,
deny_nico: true,
deny_deprecated: false,
with_sections: false
def self.normalise_tags! tag_names, with_tagme: true,
with_no_deerjikist: true,
deny_nico: true,
deny_deprecated: false,
with_sections: false
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
raise NicoTagNormalisationError
end
@@ -148,7 +145,7 @@ class Tag < ApplicationRecord
name = TagName.canonicalise(name).first
find_or_create_by_tag_name!(locale, name, category: (cat || :general)).tap do |tag|
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
if deny_deprecated && tag.deprecated?
raise DeprecatedTagNormalisationError, [tag.name]
end
@@ -236,23 +233,13 @@ class Tag < ApplicationRecord
[left_end_ms, right_end_ms].max
end
def self.find_or_create_by_tag_name! locale, name, category:
language_code = locale.language_code
name = name.to_s.strip
def self.find_or_create_by_tag_name! name, category:
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
tn = tn.canonical if tn.canonical_id?
tn = TagName.find_or_create_by!(language_code:, name:) do
_1.script_code = locale.script_code
_1.primary_flg = true
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
t.category = category
end
tag = tn.tag
unless tag
tag = Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do
_1.category = category
end
end
tag
rescue ActiveRecord::RecordNotUnique
retry
end
+1 -7
ファイルの表示
@@ -1,7 +1,7 @@
class TagName < ApplicationRecord
include MyDiscard
belongs_to :tag
has_one :tag
has_one :wiki_page
belongs_to :canonical, class_name: 'TagName', optional: true
@@ -23,12 +23,6 @@ class TagName < ApplicationRecord
names.map { |name| tns[name]&.canonical&.name || name }.uniq
end
def self.generate_name locale, tag, name
# TODO: 言語ごとの自動命名ロジック完成したら書く.
"Tag_##{ tag.id }"
end
private
def canonical_must_be_canonical
-1
ファイルの表示
@@ -25,7 +25,6 @@ class PostVersionRecorder < VersionRecorder
thumbnail_base: @record.thumbnail_base,
video_ms: @record.video_ms,
tags: @record.snapshot_tag_names.join(' '),
tags_json: @record.snapshot_tags_json,
parent_post_ids: @record.snapshot_parent_post_ids.join(' '),
original_created_from: @record.original_created_from,
original_created_before: @record.original_created_before }
-299
ファイルの表示
@@ -1,299 +0,0 @@
class AddTagsJsonToPostVersions < ActiveRecord::Migration[8.0]
RESOLUTION_GRACE = 1.second
SECTION_LITERAL_PATTERN = /\[[^\[\]\s]*-[^\[\]\s]*\]\z/
class MigrationPostVersion < ActiveRecord::Base
self.table_name = 'post_versions'
end
class MigrationTag < ActiveRecord::Base
self.table_name = 'tags'
end
class MigrationTagName < ActiveRecord::Base
self.table_name = 'tag_names'
end
class MigrationTagVersion < ActiveRecord::Base
self.table_name = 'tag_versions'
end
class MigrationNicoTagVersion < ActiveRecord::Base
self.table_name = 'nico_tag_versions'
end
def up
add_column :post_versions, :tags_json, :json, after: :tags
MigrationPostVersion.reset_column_information
backfill_missing_initial_tag_versions!
intervals_by_name = build_intervals_by_name
say_with_time 'Backfilling post_versions.tags_json' do
MigrationPostVersion.where(tags_json: nil).find_each(batch_size: 500) do |version|
version.update_columns(tags_json: build_tags_json(version, intervals_by_name))
end
end
change_column_null :post_versions, :tags_json, false
schema = connection.quote(JSON.generate({
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 } }))
add_check_constraint :post_versions,
"JSON_SCHEMA_VALID(#{ schema }, tags_json)",
name: 'chk_post_versions_tags_json_schema'
end
def down
remove_check_constraint :post_versions, name: 'chk_post_versions_tags_json_schema'
remove_column :post_versions, :tags_json
end
private
def backfill_missing_initial_tag_versions!
say_with_time 'Backfilling missing initial tag versions' do
tag_rows = missing_initial_version_rows(MigrationTagVersion, nico: false)
nico_rows = missing_initial_version_rows(MigrationNicoTagVersion, nico: true)
MigrationTagVersion.insert_all!(tag_rows) if tag_rows.any?
MigrationNicoTagVersion.insert_all!(nico_rows) if nico_rows.any?
tag_rows.length + nico_rows.length
end
end
def missing_initial_version_rows version_class, nico:
first_versions =
version_class
.order(:tag_id, :version_no)
.to_a
.group_by(&:tag_id)
.transform_values(&:first)
rows = []
MigrationTag.find_each do |tag|
next if (tag.category == 'nico') != nico
first_version = first_versions[tag.id]
next unless first_version
next if valid_initial_version?(first_version)
assert_inferable_initial_version!(tag, first_version)
rows << initial_version_row(tag, first_version, nico:)
end
rows
end
def valid_initial_version? version
version.version_no == 1 && version.event_type == 'create'
end
def assert_inferable_initial_version! tag, version
inferable =
version.version_no == 2 &&
version.event_type == 'discard' &&
tag.created_at < version.created_at
return if inferable
details = [
"tag_id=#{ tag.id }",
"version_no=#{ version.version_no }",
"event_type=#{ version.event_type.inspect }",
"tag_created_at=#{ tag.created_at.iso8601(6) }",
"version_created_at=#{ version.created_at.iso8601(6) }"]
raise "Cannot infer initial tag version: #{ details.join(', ') }"
end
def initial_version_row tag, discard_version, nico:
row = {
tag_id: tag.id,
version_no: 1,
event_type: 'create',
name: discard_version.name,
created_at: tag.created_at,
created_by_user_id: nil }
if nico
return row.merge(linked_tags: discard_version.linked_tags)
end
row.merge(
category: discard_version.category,
aliases: discard_version.aliases,
parent_tag_ids: discard_version.parent_tag_ids,
deprecated_at: discard_version.deprecated_at)
end
def build_intervals_by_name
intervals_by_name = Hash.new { |hash, name| hash[name] = [] }
versions_by_kind = {
tag: versions_by_tag_id(MigrationTagVersion),
nico: versions_by_tag_id(MigrationNicoTagVersion) }
current_names = current_names_by_tag_id
MigrationTag.find_each do |tag|
nico = tag.category == 'nico'
kind = nico ? :nico : :tag
versions = versions_by_kind.fetch(kind).fetch(tag.id, [])
intervals_for(
tag,
versions,
current_name: current_names.fetch(tag.id),
nico:).each do |interval|
name = interval.delete(:name)
intervals_by_name[name] << interval
end
end
intervals_by_name
end
def versions_by_tag_id version_class
version_class
.order(:tag_id, :version_no)
.to_a
.group_by(&:tag_id)
end
def current_names_by_tag_id
MigrationTagName
.joins('INNER JOIN tags ON tags.tag_name_id = tag_names.id')
.pluck('tags.id', 'tag_names.name')
.to_h
end
def intervals_for tag, versions, current_name:, nico:
if versions.empty?
return [{
name: current_name,
tag_id: tag.id,
version_no: tag.version_no,
category: nico ? 'nico' : tag.category,
from: tag.created_at,
to: tag.discarded_at }]
end
versions.each_with_index.filter_map do |version, index|
next if version.event_type == 'discard'
{
name: version.name,
tag_id: tag.id,
version_no: version.version_no,
category: nico ? 'nico' : version.category,
from: version.created_at,
to: versions[index + 1]&.created_at || tag.discarded_at }
end
end
def build_tags_json version, intervals_by_name
entries = version.tags.to_s.split.map do |literal|
name = tag_name_from_literal(literal)
interval = resolve_tag!(intervals_by_name.fetch(name, []), name:, version:)
{ 'id' => interval.fetch(:tag_id),
'version_no' => interval.fetch(:version_no),
'name' => name,
'category' => interval.fetch(:category),
'sections' => [] }
end
assert_unique_tag_ids!(version, entries)
entries.sort_by { |entry| entry.fetch('id') }
end
def tag_name_from_literal literal
name = literal.dup
name.sub!(SECTION_LITERAL_PATTERN, '') while name.match?(
SECTION_LITERAL_PATTERN)
if name.empty? || name.include?('[') || name.include?(']')
raise "Invalid legacy tag literal: #{ literal.inspect }"
end
name
end
def resolve_tag! intervals, name:, version:
time = version.created_at
candidates = intervals.select do |interval|
interval.fetch(:from) <= time &&
(interval[:to].nil? || time < interval.fetch(:to))
end
candidates = future_candidates(intervals, time) if candidates.empty?
return candidates.first if candidates.one?
candidate_versions = candidates.map do |candidate|
[candidate.fetch(:tag_id), candidate.fetch(:version_no)]
end
details = [
"post_version_id=#{ version.id }",
"post_id=#{ version.post_id }",
"name=#{ name.inspect }",
"created_at=#{ time.iso8601(6) }",
"candidates=#{ candidate_versions.inspect }"].join(', ')
raise "Could not resolve tag snapshot: #{ details }"
end
def future_candidates intervals, time
candidates = intervals.select do |interval|
interval.fetch(:from) > time &&
interval.fetch(:from) <= time + RESOLUTION_GRACE
end
return [] if candidates.empty?
nearest_from = candidates.map { |interval| interval.fetch(:from) }.min
candidates.select do |interval|
interval.fetch(:from) == nearest_from
end
end
def assert_unique_tag_ids! version, entries
duplicate_tag_ids =
entries
.map { |entry| entry.fetch('id') }
.tally
.select { |_tag_id, count| count > 1 }
.keys
return if duplicate_tag_ids.empty?
details = [
"post_version_id=#{ version.id }",
"duplicate_tag_ids=#{ duplicate_tag_ids.inspect }"].join(', ')
raise "Duplicate tag IDs: #{ details }"
end
end
-20
ファイルの表示
@@ -1,20 +0,0 @@
class CreateLanguages < ActiveRecord::Migration[8.0]
def up
create_table :languages, id: { type: :string, limit: 16 }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
end
execute <<~SQL
INSERT INTO
languages(code, name, created_at)
VALUES
('ja', '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :languages
end
end
-20
ファイルの表示
@@ -1,20 +0,0 @@
class CreateScripts < ActiveRecord::Migration[8.0]
def up
create_table :scripts, id: { type: 'CHAR(4)' }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
end
execute <<~SQL
INSERT INTO
scripts(code, name, created_at)
VALUES
('Jpan', '漢字および仮名文字', #{ connection.quote Time.current })
SQL
end
def down
drop_table :scripts
end
end
-25
ファイルの表示
@@ -1,25 +0,0 @@
class CreateLocales < ActiveRecord::Migration[8.0]
def up
create_table :locales, id: { type: :string, limit: 32 }, primary_key: :code do |t|
t.string :language_code, limit: 16, null: false, index: true
t.column :script_code, 'CHAR(4)', null: false
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
t.foreign_key :languages, column: :language_code, primary_key: :code
t.foreign_key :scripts, column: :script_code, primary_key: :code
end
execute <<~SQL
INSERT INTO
locales(code, language_code, script_code, name, created_at)
VALUES
('ja', 'ja', 'Jpan', '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :locales
end
end
-59
ファイルの表示
@@ -1,59 +0,0 @@
class AddColumnsToTagNames < ActiveRecord::Migration[8.0]
def up
add_reference :tag_names, :tag, after: :id, foreign_key: true
add_column :tag_names, :language_code, :string,
limit: 16, null: false, after: :tag_id, default: 'ja'
add_column :tag_names, :script_code, 'CHAR(4)',
null: false, after: :name, default: 'Jpan'
add_column :tag_names, :primary_flg, :boolean,
null: false, after: :script_code, default: true
add_foreign_key :tag_names, :languages, column: :language_code, primary_key: :code
add_foreign_key :tag_names, :scripts, column: :script_code, primary_key: :code
remove_index :tag_names, :name
add_index :tag_names, [:language_code, :name], unique: true
change_column_default :tag_names, :language_code, from: 'ja', to: nil
change_column_default :tag_names, :script_code, from: 'Jpan', to: nil
execute <<~SQL
UPDATE
tag_names tn
LEFT JOIN
tags AS t
ON
t.tag_name_id = COALESCE(tn.canonical_id, tn.id)
SET
tn.tag_id = t.id
, tn.primary_flg = CASE
WHEN tn.canonical_id IS NULL THEN
1
ELSE
0
END
SQL
change_column_default :tag_names, :primary_flg, from: true, to: nil
add_column :tag_names, :primary_tag_id, :bigint,
as: 'CASE WHEN primary_flg THEN tag_id ELSE NULL END'
add_index :tag_names, [:primary_tag_id, :language_code], unique: true
end
def down
remove_index :tag_names, [:primary_tag_id, :language_code]
remove_column :tag_names, :primary_tag_id
remove_foreign_key :tag_names, column: :language_code
remove_foreign_key :tag_names, column: :script_code
remove_index :tag_names, [:language_code, :name]
add_index :tag_names, :name, unique: true
remove_column :tag_names, :primary_flg
remove_column :tag_names, :script_code
remove_column :tag_names, :language_code
remove_reference :tag_names, :tag, foreign_key: true
end
end
生成ファイル
+34 -15
ファイルの表示
@@ -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_07_27_123600) do
ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) 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
@@ -72,6 +72,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.index ["correct_post_id"], name: "index_gekanator_games_on_correct_post_id"
t.index ["guessed_post_id"], name: "index_gekanator_games_on_guessed_post_id"
t.index ["user_id"], name: "index_gekanator_games_on_user_id"
t.check_constraint "`question_count` >= 0", name: "chk_gekanator_games_question_count_nonnegative"
end
create_table "gekanator_question_examples", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -274,9 +275,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.datetime "created_at", null: false
t.bigint "created_by_user_id"
t.index ["created_at"], name: "index_nico_tag_versions_on_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at"
t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "version_no"], name: "index_nico_tag_versions_on_tag_id_and_version_no", unique: true
t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive"
end
create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -285,13 +287,14 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["parent_post_id"], name: "index_post_implications_on_parent_post_id"
t.check_constraint "`post_id` <> `parent_post_id`", name: "chk_post_implications_no_self"
end
create_table "post_similarities", primary_key: ["post_id", "target_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "post_id", null: false
t.bigint "target_post_id", null: false
t.float "cos", null: false
t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos"
t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos", order: { cos: :desc }
t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id"
end
@@ -346,7 +349,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.string "url", limit: 768, null: false
t.string "thumbnail_base", limit: 2000
t.text "tags", null: false
t.json "tags_json", null: false
t.text "parent_post_ids", null: false
t.datetime "original_created_from"
t.datetime "original_created_before"
@@ -358,7 +360,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.index ["post_id"], name: "index_post_versions_on_post_id"
t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id"
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive"
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\",\"nico\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"id\",\"version_no\",\"name\",\"category\",\"sections\"],\"additionalProperties\":false}}',`tags_json`)", name: "chk_post_versions_tags_json_schema"
t.check_constraint "`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"
end
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -376,6 +379,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.index ["url"], name: "index_posts_on_url", unique: true
t.index ["video_ms", "id"], name: "idx_posts_video_ms_id"
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_posts_video_ms_positive"
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
end
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -383,18 +387,25 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "theme", default: "system", null: false
t.string "display_density", default: "comfortable", null: false
t.string "font_size", default: "normal", null: false
t.integer "post_list_limit", default: 50, null: false
t.string "post_list_order", default: "created_at_desc", null: false
t.string "viewed_post_display", default: "show", null: false
t.boolean "tag_autocomplete_nico", default: true, null: false
t.string "auto_fetch_title", default: "manual", null: false
t.string "auto_fetch_thumbnail", default: "manual", null: false
t.string "wiki_editor_mode", default: "split", null: false
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
end
create_table "wiki_assets", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "wiki_page_id", null: false
t.integer "no", null: false
t.string "alt_text"
t.binary "sha256", limit: 32, null: false
t.bigint "created_by_user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["created_by_user_id"], name: "index_wiki_assets_on_created_by_user_id"
t.index ["wiki_page_id", "no"], name: "index_wiki_assets_on_wiki_page_id_and_no", unique: true
t.index ["wiki_page_id", "sha256"], name: "index_wiki_assets_on_wiki_page_id_and_sha256", unique: true
end
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "tag_id", null: false
t.bigint "parent_tag_id", null: false
@@ -430,7 +441,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.bigint "tag_id", null: false
t.bigint "target_tag_id", null: false
t.float "cos", null: false
t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos"
t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos", order: { cos: :desc }
t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id"
end
@@ -446,9 +457,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.datetime "created_at", null: false
t.bigint "created_by_user_id"
t.index ["created_at"], name: "index_tag_versions_on_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at"
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive"
end
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -464,6 +476,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
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"
end
create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -617,11 +630,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "discarded_at"
t.integer "next_asset_no", default: 1, null: false
t.integer "version_no", null: false
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
t.check_constraint "`version_no` > 0", name: "chk_wiki_pages_version_no_positive"
end
create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -666,6 +681,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
t.index ["created_by_user_id"], name: "index_wiki_versions_on_created_by_user_id"
t.index ["wiki_page_id", "version_no"], name: "index_wiki_versions_on_wiki_page_id_and_version_no", unique: true
t.index ["wiki_page_id"], name: "index_wiki_versions_on_wiki_page_id"
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "wiki_versions_event_type_valid"
t.check_constraint "`version_no` > 0", name: "wiki_versions_version_no_positive"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
@@ -747,6 +764,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
add_foreign_key "user_post_views", "posts"
add_foreign_key "user_post_views", "users"
add_foreign_key "user_theme_slots", "users"
add_foreign_key "wiki_assets", "users", column: "created_by_user_id"
add_foreign_key "wiki_assets", "wiki_pages"
add_foreign_key "wiki_pages", "tag_names"
add_foreign_key "wiki_pages", "users", column: "created_user_id"
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
-1
ファイルの表示
@@ -19,7 +19,6 @@ RSpec.describe PostVersion, type: :model do
url: post_record.url,
thumbnail_base: post_record.thumbnail_base,
tags: post_record.snapshot_tag_names.join(' '),
tags_json: post_record.snapshot_tags_json,
parent_post_ids: post_record.snapshot_parent_post_ids.join(' '),
original_created_from: post_record.original_created_from,
original_created_before: post_record.original_created_before,
-1
ファイルの表示
@@ -333,7 +333,6 @@ RSpec.describe Tag, type: :model do
url: post.url,
thumbnail_base: post.thumbnail_base,
tags: snapshot_tags(post),
tags_json: post.snapshot_tags_json,
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
-2
ファイルの表示
@@ -55,7 +55,6 @@ RSpec.describe 'Posts API', type: :request do
thumbnail_base: post.thumbnail_base,
video_ms: post.video_ms,
tags: post.snapshot_tag_names.join(' '),
tags_json: post.snapshot_tags_json,
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
@@ -2025,7 +2024,6 @@ RSpec.describe 'Posts API', type: :request do
url: post.url,
thumbnail_base: post.thumbnail_base,
tags: snapshot_tags(post),
tags_json: post.snapshot_tags_json,
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
-83
ファイルの表示
@@ -226,14 +226,6 @@ RSpec.describe 'Tags deerjikists API', type: :request do
[platform2, code2],
)
end
it 'locks the tag before replacing the complete list' do
expect_any_instance_of(Tag).to receive(:lock!).and_call_original
do_request
expect(response).to have_http_status(:ok)
end
end
context 'when tag already has deerjikists' do
@@ -307,81 +299,6 @@ RSpec.describe 'Tags deerjikists API', type: :request do
end
end
context 'when platform is outside the enum' do
let(:payload) do
[
{ platform: 'invalid', code: code1 },
]
end
it 'returns 422 with an indexed platform error without changing the list' do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
expect {
do_request
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'deerjikists.0.platform' => [be_present],
)
end
end
context 'when a requested deerjikist belongs to another tag' do
let!(:other_tag) { create(:tag, category: :deerjikist) }
let!(:owned_deerjikist) do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
end
let!(:conflicting_deerjikist) do
Deerjikist.create!(platform: platform2, code: code2, tag: other_tag)
end
let(:payload) do
[
{ platform: 'nico', code: 'new-code' },
{ platform: platform2, code: code2 },
]
end
before do
other_tag.tag_name.update!(name: 'existing-deerjikist')
end
it 'returns an indexed 422 error and rolls back the complete replacement' do
expect {
do_request
}.not_to change { Deerjikist.order(:platform, :code).pluck(:platform, :code, :tag_id) }
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'deerjikists.1.code' => [include('existing-deerjikist')],
)
expect(owned_deerjikist.reload.tag_id).to eq(tag.id)
expect(conflicting_deerjikist.reload.tag_id).to eq(other_tag.id)
end
end
context 'when a requested deerjikist already belongs to the same tag' do
let!(:existing_deerjikist) do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
end
let(:payload) do
[
{ platform: platform1, code: code1 },
]
end
it 'keeps the existing row' do
expect {
do_request
}.not_to change { existing_deerjikist.reload.created_at }
expect(response).to have_http_status(:ok)
expect(Deerjikist.where(tag: tag).pluck(:platform, :code))
.to eq([[platform1, code1]])
end
end
context 'when youtube code is handle' do
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
let(:payload) do
-1
ファイルの表示
@@ -156,7 +156,6 @@ RSpec.describe 'nico:sync' do
url: post.url,
thumbnail_base: post.thumbnail_base,
tags: snapshot_tags(post),
tags_json: post.snapshot_tags_json,
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,