コミットを比較
6
コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
b0df11c500 | ||
|
|
886128a7a8 | ||
|
|
6d1a8f0444 | ||
|
|
a947032247 | ||
|
|
3115e31fb1 | ||
|
|
6f0442d633 |
@@ -85,4 +85,11 @@ class ApplicationController < ActionController::API
|
|||||||
base_errors: },
|
base_errors: },
|
||||||
status:
|
status:
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalise_json value
|
||||||
|
return nil if value.nil?
|
||||||
|
return JSON.parse(value) if value.is_a?(String)
|
||||||
|
|
||||||
|
value
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -236,10 +236,10 @@ class MaterialsController < ApplicationController
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_material_tag! tag_name_raw
|
def resolve_material_tag! locale, tag_name_raw
|
||||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
tag_name = TagName.find_undiscard_or_create_by!(language_code: locale.language_code,
|
||||||
tag = tag_name.tag
|
name: tag_name_raw)
|
||||||
tag || Tag.create!(tag_name:, category: :material)
|
tag_name.tag || Tag.create!(tag_name:, category: :material)
|
||||||
end
|
end
|
||||||
|
|
||||||
def material_index_needs_tag_name? filters
|
def material_index_needs_tag_name? filters
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class PostVersionsController < ApplicationController
|
class PostVersionsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
post_id = params[:post].presence
|
post_id = params[:post].presence
|
||||||
tag_id = params[:tag].presence
|
tag_id = params[:tag].presence&.to_i
|
||||||
page = (params[:page].presence || 1).to_i
|
page = (params[:page].presence || 1).to_i
|
||||||
limit = (params[:limit].presence || 20).to_i
|
limit = (params[:limit].presence || 20).to_i
|
||||||
|
|
||||||
@@ -10,12 +10,6 @@ class PostVersionsController < ApplicationController
|
|||||||
|
|
||||||
offset = (page - 1) * limit
|
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)
|
q = PostVersion.joins(<<~SQL.squish)
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
post_versions prev
|
post_versions prev
|
||||||
@@ -23,17 +17,18 @@ class PostVersionsController < ApplicationController
|
|||||||
prev.post_id = post_versions.post_id
|
prev.post_id = post_versions.post_id
|
||||||
AND prev.version_no = post_versions.version_no - 1
|
AND prev.version_no = post_versions.version_no - 1
|
||||||
SQL
|
SQL
|
||||||
.select('post_versions.*', 'prev.title AS prev_title', 'prev.url AS prev_url',
|
.select('post_versions.*',
|
||||||
'prev.thumbnail_base AS prev_thumbnail_base', 'prev.tags AS prev_tags',
|
'prev.title AS prev_title',
|
||||||
|
'prev.url AS prev_url',
|
||||||
|
'prev.thumbnail_base AS prev_thumbnail_base',
|
||||||
|
'prev.tags_json AS prev_tags_json',
|
||||||
'prev.video_ms AS prev_video_ms',
|
'prev.video_ms AS prev_video_ms',
|
||||||
'prev.original_created_from AS prev_original_created_from',
|
'prev.original_created_from AS prev_original_created_from',
|
||||||
'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_name
|
if tag_id
|
||||||
escaped = ActiveRecord::Base.sanitize_sql_like(tag_name.name)
|
q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('id', #{ tag_id })) " +
|
||||||
q = q.where(("CONCAT(' ', post_versions.tags, ' ') LIKE :kw " +
|
"OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('id', #{ tag_id }))")
|
||||||
"OR CONCAT(' ', prev.tags, ' ') LIKE :kw"),
|
|
||||||
kw: "% #{ escaped } %")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
count = q.except(:select, :order, :limit, :offset).count
|
count = q.except(:select, :order, :limit, :offset).count
|
||||||
@@ -52,51 +47,69 @@ class PostVersionsController < ApplicationController
|
|||||||
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
|
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
|
||||||
|
|
||||||
rows.map do |row|
|
rows.map do |row|
|
||||||
cur_tags = split_tags(row.tags)
|
cur_tags =
|
||||||
prev_tags = split_tags(row.attributes['prev_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) }
|
||||||
|
|
||||||
{
|
{ post_id: row.post_id,
|
||||||
post_id: row.post_id,
|
|
||||||
version_no: row.version_no,
|
version_no: row.version_no,
|
||||||
event_type: row.event_type,
|
event_type: row.event_type,
|
||||||
title: {
|
title: { current: row.title, prev: row.attributes['prev_title'] },
|
||||||
current: row.title,
|
url: { current: row.url, prev: row.attributes['prev_url'] },
|
||||||
prev: row.attributes['prev_title']
|
thumbnail: { current: nil, prev: nil },
|
||||||
},
|
thumbnail_base: { current: row.thumbnail_base,
|
||||||
url: {
|
prev: row.attributes['prev_thumbnail_base'] },
|
||||||
current: row.url,
|
video_ms: { current: row.video_ms, prev: row.attributes['prev_video_ms'] },
|
||||||
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),
|
tags: build_version_tags(cur_tags, prev_tags),
|
||||||
original_created_from: {
|
original_created_from: {
|
||||||
current: row.original_created_from&.iso8601,
|
current: row.original_created_from&.iso8601,
|
||||||
prev: row.attributes['prev_original_created_from']&.iso8601
|
prev: row.attributes['prev_original_created_from']&.iso8601 },
|
||||||
},
|
|
||||||
original_created_before: {
|
original_created_before: {
|
||||||
current: row.original_created_before&.iso8601,
|
current: row.original_created_before&.iso8601,
|
||||||
prev: row.attributes['prev_original_created_before']&.iso8601
|
prev: row.attributes['prev_original_created_before']&.iso8601 },
|
||||||
},
|
|
||||||
created_at: row.created_at.iso8601,
|
created_at: row.created_at.iso8601,
|
||||||
created_by_user:
|
created_by_user:
|
||||||
if row.created_by_user_id
|
if row.created_by_user_id
|
||||||
{
|
{ id: row.created_by_user_id,
|
||||||
id: row.created_by_user_id,
|
name: users_by_id[row.created_by_user_id] }
|
||||||
name: users_by_id[row.created_by_user_id]
|
end }
|
||||||
}
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -117,8 +130,4 @@ class PostVersionsController < ApplicationController
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def split_tags(tags)
|
|
||||||
tags.to_s.split(/\s+/).reject(&:blank?)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -311,6 +311,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]
|
||||||
@@ -332,7 +333,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:,
|
||||||
@@ -361,7 +363,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
|
||||||
@@ -699,7 +701,10 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def editable_tag_names_from_version version
|
def editable_tag_names_from_version version
|
||||||
version.tags.to_s.split.reject { |name| name.downcase.start_with?('nico:') }.sort
|
version.tags_json
|
||||||
|
.reject { _1.fetch('category') == 'nico' }
|
||||||
|
.map { Post.tag_snapshot_literal(_1) }
|
||||||
|
.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_snapshot_from_record post
|
def post_snapshot_from_record post
|
||||||
@@ -730,11 +735,11 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:,
|
||||||
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
||||||
validate_original_created_values!(original_created_from, original_created_before)
|
validate_original_created_values!(original_created_from, original_created_before)
|
||||||
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
Tag.normalise_tags!(locale, tag_names, with_tagme: false, deny_deprecated: true,
|
||||||
with_sections: true) =>
|
with_sections: true) =>
|
||||||
{ tags:, sections: }
|
{ tags:, sections: }
|
||||||
|
|
||||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||||
@@ -870,7 +875,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],
|
||||||
@@ -878,10 +883,10 @@ class PostsController < ApplicationController
|
|||||||
original_created_from: snapshot[:original_created_from],
|
original_created_from: snapshot[:original_created_from],
|
||||||
original_created_before: snapshot[:original_created_before])
|
original_created_before: snapshot[:original_created_before])
|
||||||
|
|
||||||
Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false,
|
Tag.normalise_tags!(locale, snapshot[:tag_names],
|
||||||
deny_deprecated: true,
|
with_tagme: false,
|
||||||
with_sections: true) =>
|
deny_deprecated: true,
|
||||||
{ tags: editable_tags, sections: }
|
with_sections: true) => { tags: editable_tags, sections: }
|
||||||
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
|
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
|
||||||
|
|
||||||
readonly_tags = post.tags.nico.to_a
|
readonly_tags = post.tags.nico.to_a
|
||||||
|
|||||||
@@ -200,14 +200,45 @@ class TagsController < ApplicationController
|
|||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
return head :not_found unless tag
|
return head :not_found unless tag
|
||||||
|
|
||||||
|
rows = normalise_deerjikist_rows(tag)
|
||||||
|
return if performed?
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
tag.deerjikists = []
|
tag.lock!
|
||||||
params[:_json].each.with_index do |item, i|
|
|
||||||
platform = item[:platform]
|
requested_keys = rows.map { |row| [row[:platform], row[:code]] }.uniq
|
||||||
code = normalise_deerjikist_code(platform, item[:code])
|
row_indexes_by_key = rows_by_key(rows)
|
||||||
deerjikist = Deerjikist.find_or_initialize_by(platform:, code:)
|
locked_deerjikists = lock_deerjikists_for_tag_update(tag.id, requested_keys)
|
||||||
deerjikist.tag = tag
|
current_deerjikists = locked_deerjikists.filter { |deerjikist|
|
||||||
render_deerjikist_form_record_invalid(deerjikist, i) unless deerjikist.save
|
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
|
||||||
raise ActiveRecord::Rollback if performed?
|
raise ActiveRecord::Rollback if performed?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -653,6 +684,7 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def normalise_deerjikist_code platform, code
|
def normalise_deerjikist_code platform, code
|
||||||
|
code = code.to_s
|
||||||
return code if platform != 'youtube' || code[0] != '@'
|
return code if platform != 'youtube' || code[0] != '@'
|
||||||
|
|
||||||
url = "https://www.youtube.com/#{ code }"
|
url = "https://www.youtube.com/#{ code }"
|
||||||
@@ -669,6 +701,82 @@ class TagsController < ApplicationController
|
|||||||
nil
|
nil
|
||||||
end
|
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
|
def render_deerjikist_form_record_invalid deerjikist, index
|
||||||
fields = { }
|
fields = { }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class Language < ApplicationRecord
|
||||||
|
has_many :languages, class_name: 'Locale', foreign_key: :language_code
|
||||||
|
end
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
class Locale < ApplicationRecord
|
||||||
|
after_create_commit :generate_tag_names!
|
||||||
|
|
||||||
|
belongs_to :language, foreign_key: :language_code, primary_key: :code
|
||||||
|
belongs_to :script, foreign_key: :script_code, primary_key: :code
|
||||||
|
|
||||||
|
def self.nipponese = Locale.find('ja')
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_tag_names!
|
||||||
|
tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id)
|
||||||
|
Tag.where.not(id: tag_ids).find_each do
|
||||||
|
TagName.create!(tag_id: _1.id,
|
||||||
|
language_code:,
|
||||||
|
name: TagName.generate_name(self, _1, _1.name),
|
||||||
|
script_code:,
|
||||||
|
primary_flg: true,
|
||||||
|
# TODO: 公証実装したら書く.
|
||||||
|
# auto_generated: true,
|
||||||
|
canonical_id: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -137,6 +137,34 @@ class Post < ApplicationRecord
|
|||||||
end
|
end
|
||||||
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
|
def self.section_literal section
|
||||||
end_ms =
|
end_ms =
|
||||||
section.end_ms ? Post.ms_to_time(section.end_ms) : ''
|
section.end_ms ? Post.ms_to_time(section.end_ms) : ''
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class Script < ApplicationRecord
|
||||||
|
;
|
||||||
|
end
|
||||||
+30
-17
@@ -56,6 +56,7 @@ class Tag < ApplicationRecord
|
|||||||
has_many :tag_versions
|
has_many :tag_versions
|
||||||
has_many :nico_tag_versions
|
has_many :nico_tag_versions
|
||||||
|
|
||||||
|
has_many :tag_names
|
||||||
belongs_to :tag_name
|
belongs_to :tag_name
|
||||||
delegate :wiki_page, to: :tag_name
|
delegate :wiki_page, to: :tag_name
|
||||||
|
|
||||||
@@ -105,18 +106,20 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||||
|
|
||||||
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
|
def self.tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta)
|
||||||
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
|
def self.bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
|
||||||
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
|
def self.no_deerjikist =
|
||||||
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
|
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
|
||||||
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
|
def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
|
||||||
def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
|
def self.niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
|
||||||
|
def self.youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
|
||||||
|
|
||||||
def self.normalise_tags! tag_names, with_tagme: true,
|
def self.normalise_tags! locale, tag_names,
|
||||||
with_no_deerjikist: true,
|
with_tagme: true,
|
||||||
deny_nico: true,
|
with_no_deerjikist: true,
|
||||||
deny_deprecated: false,
|
deny_nico: true,
|
||||||
with_sections: false
|
deny_deprecated: false,
|
||||||
|
with_sections: false
|
||||||
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||||
raise NicoTagNormalisationError
|
raise NicoTagNormalisationError
|
||||||
end
|
end
|
||||||
@@ -145,7 +148,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
|
||||||
@@ -233,13 +236,23 @@ class Tag < ApplicationRecord
|
|||||||
[left_end_ms, right_end_ms].max
|
[left_end_ms, right_end_ms].max
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_or_create_by_tag_name! name, category:
|
def self.find_or_create_by_tag_name! locale, name, category:
|
||||||
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
language_code = locale.language_code
|
||||||
tn = tn.canonical if tn.canonical_id?
|
name = name.to_s.strip
|
||||||
|
|
||||||
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
|
tn = TagName.find_or_create_by!(language_code:, name:) do
|
||||||
t.category = category
|
_1.script_code = locale.script_code
|
||||||
|
_1.primary_flg = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tag = tn.tag
|
||||||
|
unless tag
|
||||||
|
tag = Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do
|
||||||
|
_1.category = category
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tag
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class TagName < ApplicationRecord
|
class TagName < ApplicationRecord
|
||||||
include MyDiscard
|
include MyDiscard
|
||||||
|
|
||||||
has_one :tag
|
belongs_to :tag
|
||||||
has_one :wiki_page
|
has_one :wiki_page
|
||||||
|
|
||||||
belongs_to :canonical, class_name: 'TagName', optional: true
|
belongs_to :canonical, class_name: 'TagName', optional: true
|
||||||
@@ -23,6 +23,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
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class PostVersionRecorder < VersionRecorder
|
|||||||
thumbnail_base: @record.thumbnail_base,
|
thumbnail_base: @record.thumbnail_base,
|
||||||
video_ms: @record.video_ms,
|
video_ms: @record.video_ms,
|
||||||
tags: @record.snapshot_tag_names.join(' '),
|
tags: @record.snapshot_tag_names.join(' '),
|
||||||
|
tags_json: @record.snapshot_tags_json,
|
||||||
parent_post_ids: @record.snapshot_parent_post_ids.join(' '),
|
parent_post_ids: @record.snapshot_parent_post_ids.join(' '),
|
||||||
original_created_from: @record.original_created_from,
|
original_created_from: @record.original_created_from,
|
||||||
original_created_before: @record.original_created_before }
|
original_created_before: @record.original_created_before }
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
class CreateLanguages < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
create_table :languages, id: { type: :string, limit: 16 }, primary_key: :code do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.datetime :deprecated_at, index: true
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
execute <<~SQL
|
||||||
|
INSERT INTO
|
||||||
|
languages(code, name, created_at)
|
||||||
|
VALUES
|
||||||
|
('ja', '日本語', #{ connection.quote Time.current })
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :languages
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
class CreateScripts < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
create_table :scripts, id: { type: 'CHAR(4)' }, primary_key: :code do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.datetime :deprecated_at, index: true
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
execute <<~SQL
|
||||||
|
INSERT INTO
|
||||||
|
scripts(code, name, created_at)
|
||||||
|
VALUES
|
||||||
|
('Jpan', '漢字および仮名文字', #{ connection.quote Time.current })
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :scripts
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class CreateLocales < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
create_table :locales, id: { type: :string, limit: 32 }, primary_key: :code do |t|
|
||||||
|
t.string :language_code, limit: 16, null: false, index: true
|
||||||
|
t.column :script_code, 'CHAR(4)', null: false
|
||||||
|
t.string :name, null: false
|
||||||
|
t.datetime :deprecated_at, index: true
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
|
||||||
|
t.foreign_key :languages, column: :language_code, primary_key: :code
|
||||||
|
t.foreign_key :scripts, column: :script_code, primary_key: :code
|
||||||
|
end
|
||||||
|
|
||||||
|
execute <<~SQL
|
||||||
|
INSERT INTO
|
||||||
|
locales(code, language_code, script_code, name, created_at)
|
||||||
|
VALUES
|
||||||
|
('ja', 'ja', 'Jpan', '日本語', #{ connection.quote Time.current })
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :locales
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
class AddColumnsToTagNames < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
add_reference :tag_names, :tag, after: :id, foreign_key: true
|
||||||
|
add_column :tag_names, :language_code, :string,
|
||||||
|
limit: 16, null: false, after: :tag_id, default: 'ja'
|
||||||
|
add_column :tag_names, :script_code, 'CHAR(4)',
|
||||||
|
null: false, after: :name, default: 'Jpan'
|
||||||
|
add_column :tag_names, :primary_flg, :boolean,
|
||||||
|
null: false, after: :script_code, default: true
|
||||||
|
|
||||||
|
add_foreign_key :tag_names, :languages, column: :language_code, primary_key: :code
|
||||||
|
add_foreign_key :tag_names, :scripts, column: :script_code, primary_key: :code
|
||||||
|
|
||||||
|
remove_index :tag_names, :name
|
||||||
|
add_index :tag_names, [:language_code, :name], unique: true
|
||||||
|
|
||||||
|
change_column_default :tag_names, :language_code, from: 'ja', to: nil
|
||||||
|
change_column_default :tag_names, :script_code, from: 'Jpan', to: nil
|
||||||
|
|
||||||
|
execute <<~SQL
|
||||||
|
UPDATE
|
||||||
|
tag_names tn
|
||||||
|
LEFT JOIN
|
||||||
|
tags AS t
|
||||||
|
ON
|
||||||
|
t.tag_name_id = COALESCE(tn.canonical_id, tn.id)
|
||||||
|
SET
|
||||||
|
tn.tag_id = t.id
|
||||||
|
, tn.primary_flg = CASE
|
||||||
|
WHEN tn.canonical_id IS NULL THEN
|
||||||
|
1
|
||||||
|
ELSE
|
||||||
|
0
|
||||||
|
END
|
||||||
|
SQL
|
||||||
|
|
||||||
|
change_column_default :tag_names, :primary_flg, from: true, to: nil
|
||||||
|
|
||||||
|
add_column :tag_names, :primary_tag_id, :bigint,
|
||||||
|
as: 'CASE WHEN primary_flg THEN tag_id ELSE NULL END'
|
||||||
|
add_index :tag_names, [:primary_tag_id, :language_code], unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_index :tag_names, [:primary_tag_id, :language_code]
|
||||||
|
remove_column :tag_names, :primary_tag_id
|
||||||
|
|
||||||
|
remove_foreign_key :tag_names, column: :language_code
|
||||||
|
remove_foreign_key :tag_names, column: :script_code
|
||||||
|
|
||||||
|
remove_index :tag_names, [:language_code, :name]
|
||||||
|
add_index :tag_names, :name, unique: true
|
||||||
|
|
||||||
|
remove_column :tag_names, :primary_flg
|
||||||
|
remove_column :tag_names, :script_code
|
||||||
|
remove_column :tag_names, :language_code
|
||||||
|
remove_reference :tag_names, :tag, foreign_key: true
|
||||||
|
end
|
||||||
|
end
|
||||||
生成ファイル
+15
-34
@@ -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_07_13_000000) do
|
ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do
|
||||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -72,7 +72,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.index ["correct_post_id"], name: "index_gekanator_games_on_correct_post_id"
|
t.index ["correct_post_id"], name: "index_gekanator_games_on_correct_post_id"
|
||||||
t.index ["guessed_post_id"], name: "index_gekanator_games_on_guessed_post_id"
|
t.index ["guessed_post_id"], name: "index_gekanator_games_on_guessed_post_id"
|
||||||
t.index ["user_id"], name: "index_gekanator_games_on_user_id"
|
t.index ["user_id"], name: "index_gekanator_games_on_user_id"
|
||||||
t.check_constraint "`question_count` >= 0", name: "chk_gekanator_games_question_count_nonnegative"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "gekanator_question_examples", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "gekanator_question_examples", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -275,10 +274,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.bigint "created_by_user_id"
|
t.bigint "created_by_user_id"
|
||||||
t.index ["created_at"], name: "index_nico_tag_versions_on_created_at"
|
t.index ["created_at"], name: "index_nico_tag_versions_on_created_at"
|
||||||
t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
|
t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at"
|
||||||
t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
|
t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at"
|
||||||
t.index ["tag_id", "version_no"], name: "index_nico_tag_versions_on_tag_id_and_version_no", unique: true
|
t.index ["tag_id", "version_no"], name: "index_nico_tag_versions_on_tag_id_and_version_no", unique: true
|
||||||
t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -287,14 +285,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["parent_post_id"], name: "index_post_implications_on_parent_post_id"
|
t.index ["parent_post_id"], name: "index_post_implications_on_parent_post_id"
|
||||||
t.check_constraint "`post_id` <> `parent_post_id`", name: "chk_post_implications_no_self"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "post_similarities", primary_key: ["post_id", "target_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "post_similarities", primary_key: ["post_id", "target_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "post_id", null: false
|
t.bigint "post_id", null: false
|
||||||
t.bigint "target_post_id", null: false
|
t.bigint "target_post_id", null: false
|
||||||
t.float "cos", null: false
|
t.float "cos", null: false
|
||||||
t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos", order: { cos: :desc }
|
t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos"
|
||||||
t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id"
|
t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -349,6 +346,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.string "url", limit: 768, null: false
|
t.string "url", limit: 768, null: false
|
||||||
t.string "thumbnail_base", limit: 2000
|
t.string "thumbnail_base", limit: 2000
|
||||||
t.text "tags", null: false
|
t.text "tags", null: false
|
||||||
|
t.json "tags_json", null: false
|
||||||
t.text "parent_post_ids", null: false
|
t.text "parent_post_ids", null: false
|
||||||
t.datetime "original_created_from"
|
t.datetime "original_created_from"
|
||||||
t.datetime "original_created_before"
|
t.datetime "original_created_before"
|
||||||
@@ -360,8 +358,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.index ["post_id"], name: "index_post_versions_on_post_id"
|
t.index ["post_id"], name: "index_post_versions_on_post_id"
|
||||||
t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id"
|
t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id"
|
||||||
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive"
|
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive"
|
||||||
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "post_versions_event_type_valid"
|
t.check_constraint "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 "`version_no` > 0", name: "post_versions_version_no_positive"
|
|
||||||
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|
|
||||||
@@ -379,7 +376,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.index ["url"], name: "index_posts_on_url", unique: true
|
t.index ["url"], name: "index_posts_on_url", unique: true
|
||||||
t.index ["video_ms", "id"], name: "idx_posts_video_ms_id"
|
t.index ["video_ms", "id"], name: "idx_posts_video_ms_id"
|
||||||
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_posts_video_ms_positive"
|
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_posts_video_ms_positive"
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -387,25 +383,18 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "theme", default: "system", null: false
|
t.string "theme", default: "system", null: false
|
||||||
|
t.string "display_density", default: "comfortable", null: false
|
||||||
|
t.string "font_size", default: "normal", null: false
|
||||||
|
t.integer "post_list_limit", default: 50, null: false
|
||||||
|
t.string "post_list_order", default: "created_at_desc", null: false
|
||||||
|
t.string "viewed_post_display", default: "show", null: false
|
||||||
|
t.boolean "tag_autocomplete_nico", default: true, null: false
|
||||||
t.string "auto_fetch_title", default: "manual", null: false
|
t.string "auto_fetch_title", default: "manual", null: false
|
||||||
t.string "auto_fetch_thumbnail", default: "manual", null: false
|
t.string "auto_fetch_thumbnail", default: "manual", null: false
|
||||||
t.string "wiki_editor_mode", default: "split", null: false
|
t.string "wiki_editor_mode", default: "split", null: false
|
||||||
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
||||||
end
|
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|
|
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "parent_tag_id", null: false
|
t.bigint "parent_tag_id", null: false
|
||||||
@@ -441,7 +430,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "target_tag_id", null: false
|
t.bigint "target_tag_id", null: false
|
||||||
t.float "cos", null: false
|
t.float "cos", null: false
|
||||||
t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos", order: { cos: :desc }
|
t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos"
|
||||||
t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id"
|
t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -457,10 +446,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.bigint "created_by_user_id"
|
t.bigint "created_by_user_id"
|
||||||
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
||||||
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
|
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at"
|
||||||
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
|
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at"
|
||||||
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
|
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
|
||||||
t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -476,7 +464,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.index ["discarded_at"], name: "index_tags_on_discarded_at"
|
t.index ["discarded_at"], name: "index_tags_on_discarded_at"
|
||||||
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
||||||
t.check_constraint "(`deprecated_at` is null) or (`category` <> _utf8mb4'nico')", name: "chk_tags_deprecated_at_not_nico"
|
t.check_constraint "(`deprecated_at` is null) or (`category` <> _utf8mb4'nico')", name: "chk_tags_deprecated_at_not_nico"
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_tags_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -630,13 +617,11 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.datetime "discarded_at"
|
t.datetime "discarded_at"
|
||||||
t.integer "next_asset_no", default: 1, null: false
|
|
||||||
t.integer "version_no", null: false
|
t.integer "version_no", null: false
|
||||||
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
||||||
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
||||||
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
|
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
|
||||||
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_wiki_pages_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -681,8 +666,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.index ["created_by_user_id"], name: "index_wiki_versions_on_created_by_user_id"
|
t.index ["created_by_user_id"], name: "index_wiki_versions_on_created_by_user_id"
|
||||||
t.index ["wiki_page_id", "version_no"], name: "index_wiki_versions_on_wiki_page_id_and_version_no", unique: true
|
t.index ["wiki_page_id", "version_no"], name: "index_wiki_versions_on_wiki_page_id_and_version_no", unique: true
|
||||||
t.index ["wiki_page_id"], name: "index_wiki_versions_on_wiki_page_id"
|
t.index ["wiki_page_id"], name: "index_wiki_versions_on_wiki_page_id"
|
||||||
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "wiki_versions_event_type_valid"
|
|
||||||
t.check_constraint "`version_no` > 0", name: "wiki_versions_version_no_positive"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
@@ -764,8 +747,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
add_foreign_key "user_post_views", "posts"
|
add_foreign_key "user_post_views", "posts"
|
||||||
add_foreign_key "user_post_views", "users"
|
add_foreign_key "user_post_views", "users"
|
||||||
add_foreign_key "user_theme_slots", "users"
|
add_foreign_key "user_theme_slots", "users"
|
||||||
add_foreign_key "wiki_assets", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "wiki_assets", "wiki_pages"
|
|
||||||
add_foreign_key "wiki_pages", "tag_names"
|
add_foreign_key "wiki_pages", "tag_names"
|
||||||
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
||||||
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ RSpec.describe PostVersion, type: :model do
|
|||||||
url: post_record.url,
|
url: post_record.url,
|
||||||
thumbnail_base: post_record.thumbnail_base,
|
thumbnail_base: post_record.thumbnail_base,
|
||||||
tags: post_record.snapshot_tag_names.join(' '),
|
tags: post_record.snapshot_tag_names.join(' '),
|
||||||
|
tags_json: post_record.snapshot_tags_json,
|
||||||
parent_post_ids: post_record.snapshot_parent_post_ids.join(' '),
|
parent_post_ids: post_record.snapshot_parent_post_ids.join(' '),
|
||||||
original_created_from: post_record.original_created_from,
|
original_created_from: post_record.original_created_from,
|
||||||
original_created_before: post_record.original_created_before,
|
original_created_before: post_record.original_created_before,
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
url: post.url,
|
url: post.url,
|
||||||
thumbnail_base: post.thumbnail_base,
|
thumbnail_base: post.thumbnail_base,
|
||||||
tags: snapshot_tags(post),
|
tags: snapshot_tags(post),
|
||||||
|
tags_json: post.snapshot_tags_json,
|
||||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||||
original_created_from: post.original_created_from,
|
original_created_from: post.original_created_from,
|
||||||
original_created_before: post.original_created_before,
|
original_created_before: post.original_created_before,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
thumbnail_base: post.thumbnail_base,
|
thumbnail_base: post.thumbnail_base,
|
||||||
video_ms: post.video_ms,
|
video_ms: post.video_ms,
|
||||||
tags: post.snapshot_tag_names.join(' '),
|
tags: post.snapshot_tag_names.join(' '),
|
||||||
|
tags_json: post.snapshot_tags_json,
|
||||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||||
original_created_from: post.original_created_from,
|
original_created_from: post.original_created_from,
|
||||||
original_created_before: post.original_created_before,
|
original_created_before: post.original_created_before,
|
||||||
@@ -2024,6 +2025,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
url: post.url,
|
url: post.url,
|
||||||
thumbnail_base: post.thumbnail_base,
|
thumbnail_base: post.thumbnail_base,
|
||||||
tags: snapshot_tags(post),
|
tags: snapshot_tags(post),
|
||||||
|
tags_json: post.snapshot_tags_json,
|
||||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||||
original_created_from: post.original_created_from,
|
original_created_from: post.original_created_from,
|
||||||
original_created_before: post.original_created_before,
|
original_created_before: post.original_created_before,
|
||||||
|
|||||||
@@ -226,6 +226,14 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
|||||||
[platform2, code2],
|
[platform2, code2],
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'when tag already has deerjikists' do
|
context 'when tag already has deerjikists' do
|
||||||
@@ -299,6 +307,81 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
|||||||
end
|
end
|
||||||
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
|
context 'when youtube code is handle' do
|
||||||
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
|
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
|
||||||
let(:payload) do
|
let(:payload) do
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ RSpec.describe 'nico:sync' do
|
|||||||
url: post.url,
|
url: post.url,
|
||||||
thumbnail_base: post.thumbnail_base,
|
thumbnail_base: post.thumbnail_base,
|
||||||
tags: snapshot_tags(post),
|
tags: snapshot_tags(post),
|
||||||
|
tags_json: post.snapshot_tags_json,
|
||||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||||
original_created_from: post.original_created_from,
|
original_created_from: post.original_created_from,
|
||||||
original_created_before: post.original_created_before,
|
original_created_before: post.original_created_before,
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする