コミットを比較
3
コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
8032ca1ccc | ||
|
|
9c78a96ac9 | ||
|
|
cc9c1e5aee |
@@ -85,11 +85,4 @@ 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! locale, tag_name_raw
|
def resolve_material_tag! tag_name_raw
|
||||||
tag_name = TagName.find_or_create_by!(language_code: locale.language_code,
|
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||||
name: tag_name_raw)
|
tag = tag_name.tag
|
||||||
tag_name.tag || Tag.create!(tag_name:, category: :material)
|
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&.to_i
|
tag_id = params[:tag].presence
|
||||||
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,6 +10,12 @@ 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
|
||||||
@@ -17,18 +23,17 @@ 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.*',
|
.select('post_versions.*', 'prev.title AS prev_title', 'prev.url AS prev_url',
|
||||||
'prev.title AS prev_title',
|
'prev.thumbnail_base AS prev_thumbnail_base', 'prev.tags AS prev_tags',
|
||||||
'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_id
|
if tag_name
|
||||||
q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('id', #{ tag_id })) " +
|
escaped = ActiveRecord::Base.sanitize_sql_like(tag_name.name)
|
||||||
"OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('id', #{ tag_id }))")
|
q = q.where(("CONCAT(' ', post_versions.tags, ' ') LIKE :kw " +
|
||||||
|
"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
|
||||||
@@ -47,69 +52,51 @@ 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 =
|
cur_tags = split_tags(row.tags)
|
||||||
normalise_json(row.tags_json)
|
prev_tags = split_tags(row.attributes['prev_tags'])
|
||||||
.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: { current: row.title, prev: row.attributes['prev_title'] },
|
title: {
|
||||||
url: { current: row.url, prev: row.attributes['prev_url'] },
|
current: row.title,
|
||||||
thumbnail: { current: nil, prev: nil },
|
prev: row.attributes['prev_title']
|
||||||
thumbnail_base: { current: row.thumbnail_base,
|
},
|
||||||
prev: row.attributes['prev_thumbnail_base'] },
|
url: {
|
||||||
video_ms: { current: row.video_ms, prev: row.attributes['prev_video_ms'] },
|
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),
|
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,
|
{
|
||||||
name: users_by_id[row.created_by_user_id] }
|
id: row.created_by_user_id,
|
||||||
end }
|
name: users_by_id[row.created_by_user_id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -130,4 +117,8 @@ class PostVersionsController < ApplicationController
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def split_tags(tags)
|
||||||
|
tags.to_s.split(/\s+/).reject(&:blank?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ class PostsController < ApplicationController
|
|||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
pt_max_sql =
|
pt_max_sql =
|
||||||
PostVersion
|
PostTag
|
||||||
.select('post_id, MAX(created_at) AS max_updated_at')
|
.select('post_id, MAX(updated_at) AS max_updated_at')
|
||||||
.group('post_id')
|
.group('post_id')
|
||||||
.to_sql
|
.to_sql
|
||||||
|
|
||||||
@@ -50,7 +50,8 @@ class PostsController < ApplicationController
|
|||||||
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
|
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
|
||||||
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
|
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
|
||||||
.preload(:uploaded_user, :parents, :children,
|
.preload(:uploaded_user, :parents, :children,
|
||||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
active_post_tags: [:sections,
|
||||||
|
{ tag: [:deerjikists, :materials,
|
||||||
{ tag_name: :wiki_page }] }])
|
{ tag_name: :wiki_page }] }])
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
|
|
||||||
@@ -103,7 +104,8 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
def random
|
def random
|
||||||
post = filtered_posts.preload(:uploaded_user, :parents, :children,
|
post = filtered_posts.preload(:uploaded_user, :parents, :children,
|
||||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
active_post_tags: [:sections,
|
||||||
|
{ tag: [:deerjikists, :materials,
|
||||||
{ tag_name: :wiki_page }] }])
|
{ tag_name: :wiki_page }] }])
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
.order('RAND()')
|
.order('RAND()')
|
||||||
@@ -188,7 +190,8 @@ class PostsController < ApplicationController
|
|||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
.includes(:uploaded_user, :parents, :children,
|
.includes(:uploaded_user, :parents, :children,
|
||||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
active_post_tags: [:sections,
|
||||||
|
{ tag: [:deerjikists, :materials,
|
||||||
{ tag_name: :wiki_page }] }])
|
{ tag_name: :wiki_page }] }])
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
@@ -308,7 +311,6 @@ 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]
|
||||||
@@ -330,8 +332,7 @@ 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(locale:,
|
incoming_snapshot = post_incoming_snapshot(title:,
|
||||||
title:,
|
|
||||||
original_created_from:,
|
original_created_from:,
|
||||||
original_created_before:,
|
original_created_before:,
|
||||||
tag_names:,
|
tag_names:,
|
||||||
@@ -360,7 +361,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
apply_post_snapshot!(locale, post, snapshot_to_apply)
|
apply_post_snapshot!(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
|
||||||
@@ -383,6 +384,50 @@ class PostsController < ApplicationController
|
|||||||
render_post_form_record_invalid e.record
|
render_post_form_record_invalid e.record
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def changes
|
||||||
|
id = params[:id].presence
|
||||||
|
tag_id = params[:tag].presence
|
||||||
|
page = (params[:page].presence || 1).to_i
|
||||||
|
limit = (params[:limit].presence || 20).to_i
|
||||||
|
|
||||||
|
page = 1 if page < 1
|
||||||
|
limit = 1 if limit < 1
|
||||||
|
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
pts = PostTag.with_discarded
|
||||||
|
pts = pts.where(post_id: id) if id.present?
|
||||||
|
pts = pts.where(tag_id:) if tag_id.present?
|
||||||
|
pts = pts.includes(:post, :created_user, :deleted_user,
|
||||||
|
tag: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||||
|
|
||||||
|
events = []
|
||||||
|
pts.each do |pt|
|
||||||
|
tag = TagRepr.base(pt.tag)
|
||||||
|
post = pt.post
|
||||||
|
|
||||||
|
events << Event.new(
|
||||||
|
post:,
|
||||||
|
tag:,
|
||||||
|
user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
|
||||||
|
change_type: 'add',
|
||||||
|
timestamp: pt.created_at)
|
||||||
|
|
||||||
|
if pt.discarded_at
|
||||||
|
events << Event.new(
|
||||||
|
post:,
|
||||||
|
tag:,
|
||||||
|
user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
|
||||||
|
change_type: 'remove',
|
||||||
|
timestamp: pt.discarded_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
events.sort_by!(&:timestamp)
|
||||||
|
events.reverse!
|
||||||
|
|
||||||
|
render json: { changes: (events.slice(offset, limit) || []).as_json, count: events.size }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filtered_posts
|
def filtered_posts
|
||||||
@@ -457,13 +502,13 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each do |pt|
|
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||||
pt.destroy!
|
pt.discard_by!(current_user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_tag_tree_for post
|
def build_tag_tree_for post
|
||||||
post_tags = post.post_tags.reject { |post_tag| post_tag.tag.deprecated? }
|
post_tags = post.active_post_tags.reject { |post_tag| post_tag.tag.deprecated? }
|
||||||
tags = post_tags.map(&:tag)
|
tags = post_tags.map(&:tag)
|
||||||
tag_ids = tags.map(&:id)
|
tag_ids = tags.map(&:id)
|
||||||
|
|
||||||
@@ -654,10 +699,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def editable_tag_names_from_version version
|
def editable_tag_names_from_version version
|
||||||
version.tags_json
|
version.tags.to_s.split.reject { |name| name.downcase.start_with?('nico:') }.sort
|
||||||
.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
|
||||||
@@ -672,6 +714,7 @@ class PostsController < ApplicationController
|
|||||||
def editable_tag_names_from_post post
|
def editable_tag_names_from_post post
|
||||||
post
|
post
|
||||||
.post_tags
|
.post_tags
|
||||||
|
.kept
|
||||||
.joins(tag: :tag_name)
|
.joins(tag: :tag_name)
|
||||||
.merge(Tag.not_nico)
|
.merge(Tag.not_nico)
|
||||||
.merge(Tag.where(deprecated_at: nil))
|
.merge(Tag.where(deprecated_at: nil))
|
||||||
@@ -687,10 +730,10 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
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:
|
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!(locale, tag_names, with_tagme: false, deny_deprecated: true,
|
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||||
with_sections: true) =>
|
with_sections: true) =>
|
||||||
{ tags:, sections: }
|
{ tags:, sections: }
|
||||||
|
|
||||||
@@ -827,7 +870,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! locale, post, snapshot
|
def apply_post_snapshot! 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],
|
||||||
@@ -835,10 +878,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!(locale, snapshot[:tag_names],
|
Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false,
|
||||||
with_tagme: false,
|
|
||||||
deny_deprecated: true,
|
deny_deprecated: true,
|
||||||
with_sections: 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
|
||||||
|
|||||||
@@ -573,7 +573,7 @@ class TagsController < ApplicationController
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
target_tag_name = TagName.find_by(name:)
|
target_tag_name = TagName.with_discarded.find_by(name:)
|
||||||
return true if target_tag_name.nil?
|
return true if target_tag_name.nil?
|
||||||
return true if target_tag_name.canonical_id?
|
return true if target_tag_name.canonical_id?
|
||||||
|
|
||||||
@@ -585,14 +585,17 @@ class TagsController < ApplicationController
|
|||||||
return if name == tag.name
|
return if name == tag.name
|
||||||
|
|
||||||
current_tag_name = tag.tag_name
|
current_tag_name = tag.tag_name
|
||||||
target_tag_name = TagName.find_by(name:)
|
target_tag_name = TagName.with_discarded.find_by(name:)
|
||||||
|
|
||||||
if target_tag_name.nil?
|
if target_tag_name.nil?
|
||||||
current_tag_name.update!(name:)
|
current_tag_name.update!(name:)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
promote_tag_alias!(tag, current_tag_name:, promoted_tag_name: target_tag_name)
|
promote_tag_alias!(
|
||||||
|
tag,
|
||||||
|
current_tag_name:,
|
||||||
|
promoted_tag_name: target_tag_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
|
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
|
||||||
@@ -602,9 +605,11 @@ class TagsController < ApplicationController
|
|||||||
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
|
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
promoted_tag_name.undiscard! if promoted_tag_name.discarded?
|
||||||
promoted_tag_name.update!(canonical: nil)
|
promoted_tag_name.update!(canonical: nil)
|
||||||
|
|
||||||
TagName.where(canonical_id: current_tag_name.id)
|
TagName.with_discarded
|
||||||
|
.where(canonical_id: current_tag_name.id)
|
||||||
.where.not(id: promoted_tag_name.id)
|
.where.not(id: promoted_tag_name.id)
|
||||||
.find_each do |alias_tag_name|
|
.find_each do |alias_tag_name|
|
||||||
alias_tag_name.update!(canonical: promoted_tag_name)
|
alias_tag_name.update!(canonical: promoted_tag_name)
|
||||||
@@ -635,7 +640,7 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
alias_names.each do |alias_name|
|
alias_names.each do |alias_name|
|
||||||
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
||||||
affected_tags << alias_tag_name.canonical&.tag
|
affected_tags << alias_tag_name.canonical&.tag
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -650,7 +655,7 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
alias_names.each do |alias_name|
|
alias_names.each do |alias_name|
|
||||||
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
||||||
alias_tag_name.update!(canonical: tag.tag_name)
|
alias_tag_name.update!(canonical: tag.tag_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class WikiPagesController < ApplicationController
|
|||||||
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
||||||
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
||||||
|
|
||||||
tag_name = TagName.find_or_create_by!(name: title)
|
tag_name = TagName.find_undiscard_or_create_by!(name: title)
|
||||||
|
|
||||||
page =
|
page =
|
||||||
Wiki::Commit.create_content!(
|
Wiki::Commit.create_content!(
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
class Language < ApplicationRecord
|
|
||||||
has_many :languages, class_name: 'Locale', foreign_key: :language_code
|
|
||||||
end
|
|
||||||
@@ -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
|
|
||||||
@@ -55,27 +55,25 @@ class Post < ApplicationRecord
|
|||||||
belongs_to :uploaded_user, class_name: 'User', optional: true
|
belongs_to :uploaded_user, class_name: 'User', optional: true
|
||||||
|
|
||||||
has_many :post_tags, dependent: :destroy, inverse_of: :post
|
has_many :post_tags, dependent: :destroy, inverse_of: :post
|
||||||
has_many :tags, through: :post_tags
|
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :post
|
||||||
|
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
|
||||||
|
has_many :tags, through: :active_post_tags
|
||||||
has_many :active_tags, -> { where(tags: { deprecated_at: nil }) },
|
has_many :active_tags, -> { where(tags: { deprecated_at: nil }) },
|
||||||
through: :post_tags,
|
through: :active_post_tags, source: :tag
|
||||||
source: :tag
|
|
||||||
|
|
||||||
has_many :user_post_views, dependent: :delete_all
|
has_many :user_post_views, dependent: :delete_all
|
||||||
has_many :post_similarities, dependent: :delete_all
|
has_many :post_similarities, dependent: :delete_all
|
||||||
has_many :post_versions
|
has_many :post_versions
|
||||||
|
|
||||||
has_many :gekanator_guessed_games,
|
has_many :gekanator_guessed_games,
|
||||||
class_name: 'GekanatorGame',
|
class_name: 'GekanatorGame',
|
||||||
foreign_key: :guessed_post_id,
|
foreign_key: :guessed_post_id,
|
||||||
dependent: :delete_all,
|
dependent: :delete_all,
|
||||||
inverse_of: :guessed_post
|
inverse_of: :guessed_post
|
||||||
|
|
||||||
has_many :gekanator_correct_games,
|
has_many :gekanator_correct_games,
|
||||||
class_name: 'GekanatorGame',
|
class_name: 'GekanatorGame',
|
||||||
foreign_key: :correct_post_id,
|
foreign_key: :correct_post_id,
|
||||||
dependent: :delete_all,
|
dependent: :delete_all,
|
||||||
inverse_of: :correct_post
|
inverse_of: :correct_post
|
||||||
|
|
||||||
has_many :gekanator_question_examples, dependent: :delete_all
|
has_many :gekanator_question_examples, dependent: :delete_all
|
||||||
|
|
||||||
has_many :parent_post_implications,
|
has_many :parent_post_implications,
|
||||||
@@ -125,6 +123,7 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
def snapshot_tag_names
|
def snapshot_tag_names
|
||||||
post_tags
|
post_tags
|
||||||
|
.kept
|
||||||
.joins(tag: :tag_name)
|
.joins(tag: :tag_name)
|
||||||
.includes(:sections, tag: :tag_name)
|
.includes(:sections, tag: :tag_name)
|
||||||
.order('tag_names.name')
|
.order('tag_names.name')
|
||||||
@@ -138,33 +137,6 @@ 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
|
|
||||||
.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) : ''
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
class PostTag < ApplicationRecord
|
class PostTag < ApplicationRecord
|
||||||
|
include Discard::Model
|
||||||
|
|
||||||
|
before_destroy do
|
||||||
|
raise ActiveRecord::ReadOnlyRecord, '消さないでください.'
|
||||||
|
end
|
||||||
|
|
||||||
belongs_to :post
|
belongs_to :post
|
||||||
belongs_to :tag, counter_cache: :post_count
|
belongs_to :tag, counter_cache: :post_count
|
||||||
belongs_to :created_user, class_name: 'User', optional: true
|
belongs_to :created_user, class_name: 'User', optional: true
|
||||||
|
belongs_to :deleted_user, class_name: 'User', optional: true
|
||||||
|
|
||||||
has_many :sections, -> { order(:begin_ms) }, class_name: 'PostTagSection',
|
has_many :sections, -> { order(:begin_ms) }, class_name: 'PostTagSection',
|
||||||
foreign_key: [:post_id, :tag_id],
|
foreign_key: [:post_id, :tag_id],
|
||||||
@@ -11,5 +18,18 @@ class PostTag < ApplicationRecord
|
|||||||
|
|
||||||
validates :post_id, presence: true
|
validates :post_id, presence: true
|
||||||
validates :tag_id, presence: true
|
validates :tag_id, presence: true
|
||||||
validates :post_id, uniqueness: { scope: :tag_id }
|
validates :post_id, uniqueness: {
|
||||||
|
scope: :tag_id,
|
||||||
|
conditions: -> { where(discarded_at: nil) } }
|
||||||
|
|
||||||
|
def discard_by! deleted_user
|
||||||
|
return self if discarded?
|
||||||
|
|
||||||
|
transaction do
|
||||||
|
update!(discarded_at: Time.current, deleted_user:)
|
||||||
|
Tag.where(id: tag_id).update_all('post_count = GREATEST(post_count - 1, 0)')
|
||||||
|
end
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class PostTagSection < ApplicationRecord
|
|||||||
belongs_to :post
|
belongs_to :post
|
||||||
belongs_to :tag
|
belongs_to :tag
|
||||||
|
|
||||||
belongs_to :post_tag, foreign_key: [:post_id, :tag_id],
|
belongs_to :post_tag, -> { kept }, foreign_key: [:post_id, :tag_id],
|
||||||
primary_key: [:post_id, :tag_id],
|
primary_key: [:post_id, :tag_id],
|
||||||
inverse_of: :sections,
|
inverse_of: :sections,
|
||||||
optional: true
|
optional: true
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
class Script < ApplicationRecord
|
|
||||||
;
|
|
||||||
end
|
|
||||||
+25
-34
@@ -2,6 +2,8 @@ require 'set'
|
|||||||
|
|
||||||
|
|
||||||
class Tag < ApplicationRecord
|
class Tag < ApplicationRecord
|
||||||
|
include MyDiscard
|
||||||
|
|
||||||
class NicoTagNormalisationError < ArgumentError
|
class NicoTagNormalisationError < ArgumentError
|
||||||
;
|
;
|
||||||
end
|
end
|
||||||
@@ -26,7 +28,9 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
has_many :post_tags, inverse_of: :tag
|
has_many :post_tags, inverse_of: :tag
|
||||||
has_many :posts, through: :post_tags
|
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag
|
||||||
|
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
|
||||||
|
has_many :posts, through: :active_post_tags
|
||||||
|
|
||||||
has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy
|
has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy
|
||||||
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
||||||
@@ -52,7 +56,6 @@ 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
|
||||||
|
|
||||||
@@ -102,16 +105,14 @@ 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!(Locale.nipponese, 'タグ希望', category: :meta)
|
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
|
||||||
def self.bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
|
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
|
||||||
def self.no_deerjikist =
|
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
|
||||||
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
|
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
|
||||||
def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
|
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
|
||||||
def self.niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
|
def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
|
||||||
def self.youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
|
|
||||||
|
|
||||||
def self.normalise_tags! locale, tag_names,
|
def self.normalise_tags! tag_names, with_tagme: true,
|
||||||
with_tagme: true,
|
|
||||||
with_no_deerjikist: true,
|
with_no_deerjikist: true,
|
||||||
deny_nico: true,
|
deny_nico: true,
|
||||||
deny_deprecated: false,
|
deny_deprecated: false,
|
||||||
@@ -144,7 +145,7 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
name = TagName.canonicalise(name).first
|
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?
|
if deny_deprecated && tag.deprecated?
|
||||||
raise DeprecatedTagNormalisationError, [tag.name]
|
raise DeprecatedTagNormalisationError, [tag.name]
|
||||||
end
|
end
|
||||||
@@ -232,23 +233,13 @@ 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! locale, name, category:
|
def self.find_or_create_by_tag_name! name, category:
|
||||||
language_code = locale.language_code
|
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
||||||
name = name.to_s.strip
|
tn = tn.canonical if tn.canonical_id?
|
||||||
|
|
||||||
tn = TagName.find_or_create_by!(language_code:, name:) do
|
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
|
||||||
_1.script_code = locale.script_code
|
t.category = category
|
||||||
_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
|
||||||
@@ -268,11 +259,11 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user:)
|
TagVersioning.ensure_snapshot!(source_tag, created_by_user:)
|
||||||
|
|
||||||
source_tag.post_tags.find_each do |source_pt|
|
source_tag.post_tags.kept.find_each do |source_pt|
|
||||||
post_id = source_pt.post_id
|
post_id = source_pt.post_id
|
||||||
affected_post_ids << post_id
|
affected_post_ids << post_id
|
||||||
source_pt.destroy!
|
source_pt.discard_by!(created_by_user)
|
||||||
unless PostTag.exists?(post_id:, tag: target_tag)
|
unless PostTag.kept.exists?(post_id:, tag: target_tag)
|
||||||
PostTag.create!(post_id:, tag: target_tag)
|
PostTag.create!(post_id:, tag: target_tag)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -284,10 +275,10 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
|
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
|
||||||
source_tag.destroy!
|
source_tag.discard!
|
||||||
|
|
||||||
if source_tag.nico?
|
if source_tag.nico?
|
||||||
source_tag_name.destroy!
|
source_tag_name.discard!
|
||||||
else
|
else
|
||||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||||
updated_at: Time.current)
|
updated_at: Time.current)
|
||||||
@@ -302,13 +293,13 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
# 投稿件数を再集計
|
# 投稿件数を再集計
|
||||||
target_tag.update_columns(post_count: PostTag.where(tag: target_tag).count)
|
target_tag.update_columns(post_count: PostTag.kept.where(tag: target_tag).count)
|
||||||
end
|
end
|
||||||
|
|
||||||
target_tag.reload
|
target_tag.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
def snapshot_aliases = tag_name.aliases.order(:name).pluck(:name)
|
def snapshot_aliases = tag_name.aliases.kept.order(:name).pluck(:name)
|
||||||
|
|
||||||
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
class TagName < ApplicationRecord
|
class TagName < ApplicationRecord
|
||||||
belongs_to :tag
|
include MyDiscard
|
||||||
|
|
||||||
|
has_one :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
|
||||||
@@ -21,12 +23,6 @@ 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
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class TagNameSanitisationRule < ApplicationRecord
|
|||||||
elsif source_tag
|
elsif source_tag
|
||||||
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
|
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
|
||||||
end
|
end
|
||||||
tn.destroy!
|
tn.discard!
|
||||||
|
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -88,14 +88,14 @@ module PostRepr
|
|||||||
|
|
||||||
def tag_json post
|
def tag_json post
|
||||||
post
|
post
|
||||||
.post_tags
|
.active_post_tags
|
||||||
.reject { _1.tag.deprecated? }
|
.reject { _1.tag.deprecated? }
|
||||||
.sort_by { _1.tag.name }
|
.sort_by { _1.tag.name }
|
||||||
.map do |post_tag|
|
.map { |post_tag|
|
||||||
TagRepr.inline(post_tag.tag).merge(
|
TagRepr.inline(post_tag.tag).merge(
|
||||||
'children' => [],
|
'children' => [],
|
||||||
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
|
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
|
||||||
end
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def thumbnail_url post, host: nil
|
def thumbnail_url post, host: nil
|
||||||
|
|||||||
@@ -121,11 +121,9 @@ class PostCreator
|
|||||||
def sync_post_tags! post, desired_tags, sections
|
def sync_post_tags! post, desired_tags, sections
|
||||||
desired_ids = desired_tags.map(&:id).to_set
|
desired_ids = desired_tags.map(&:id).to_set
|
||||||
current_ids = post.tags.pluck(:id).to_set
|
current_ids = post.tags.pluck(:id).to_set
|
||||||
|
|
||||||
Tag.where(id: desired_ids - current_ids).find_each do |tag|
|
Tag.where(id: desired_ids - current_ids).find_each do |tag|
|
||||||
PostTag.create_or_find_by!(post:, tag:, created_user: @actor)
|
PostTag.create_or_find_by!(post:, tag:, created_user: @actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
PostTagSection.where(post_id: post.id).destroy_all
|
PostTagSection.where(post_id: post.id).destroy_all
|
||||||
sections.each do |tag_id, ranges|
|
sections.each do |tag_id, ranges|
|
||||||
ranges.each do |begin_ms, end_ms|
|
ranges.each do |begin_ms, end_ms|
|
||||||
@@ -135,9 +133,10 @@ class PostCreator
|
|||||||
end_ms:)
|
end_ms:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PostTag.where(post_id: post.id,
|
PostTag.where(post_id: post.id,
|
||||||
tag_id: (current_ids - desired_ids).to_a).destroy_all
|
tag_id: (current_ids - desired_ids).to_a).kept.find_each do |post_tag|
|
||||||
|
post_tag.discard_by!(@actor)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_parent_posts! post, ids
|
def sync_parent_posts! post, ids
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ 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 }
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ module Youtube
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sync_post_tags! post, desired_tag_ids, current_tag_ids: nil
|
def sync_post_tags! post, desired_tag_ids, current_tag_ids: nil
|
||||||
current_tag_ids ||= PostTag.where(post_id: post.id).pluck(:tag_id).to_set
|
current_tag_ids ||= PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
|
||||||
desired_tag_ids = desired_tag_ids.compact.to_set
|
desired_tag_ids = desired_tag_ids.compact.to_set
|
||||||
|
|
||||||
to_add = desired_tag_ids - current_tag_ids
|
to_add = desired_tag_ids - current_tag_ids
|
||||||
@@ -117,8 +117,8 @@ module Youtube
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each do |pt|
|
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||||
pt.destroy!
|
pt.discard_by!(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Rails.application.routes.draw do
|
|||||||
get :metadata
|
get :metadata
|
||||||
post :bulk
|
post :bulk
|
||||||
get :random
|
get :random
|
||||||
|
get :changes
|
||||||
get :versions, to: 'post_versions#index'
|
get :versions, to: 'post_versions#index'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
class DeleteInactiveRecordsFromPostTags < ActiveRecord::Migration[8.0]
|
|
||||||
def up
|
|
||||||
execute <<~SQL
|
|
||||||
DELETE
|
|
||||||
FROM
|
|
||||||
post_tags
|
|
||||||
WHERE
|
|
||||||
discarded_at IS NOT NULL
|
|
||||||
SQL
|
|
||||||
|
|
||||||
remove_index :post_tags, [:tag_id, :discarded_at]
|
|
||||||
remove_index :post_tags, [:post_id, :discarded_at]
|
|
||||||
remove_index :post_tags, name: 'idx_post_tags_active_unique'
|
|
||||||
remove_index :post_tags, :discarded_at
|
|
||||||
|
|
||||||
remove_foreign_key :post_tags, column: :deleted_user_id
|
|
||||||
remove_index :post_tags, :deleted_user_id
|
|
||||||
|
|
||||||
remove_column :post_tags, :active_unique_key
|
|
||||||
remove_column :post_tags, :is_active
|
|
||||||
remove_column :post_tags, :discarded_at
|
|
||||||
remove_column :post_tags, :deleted_user_id
|
|
||||||
remove_column :post_tags, :updated_at
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
ALTER TABLE
|
|
||||||
post_tags
|
|
||||||
MODIFY COLUMN
|
|
||||||
id BIGINT NOT NULL
|
|
||||||
SQL
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
ALTER TABLE
|
|
||||||
post_tags
|
|
||||||
DROP PRIMARY KEY
|
|
||||||
SQL
|
|
||||||
|
|
||||||
remove_column :post_tags, :id
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
ALTER TABLE
|
|
||||||
post_tags
|
|
||||||
ADD PRIMARY KEY
|
|
||||||
(post_id, tag_id)
|
|
||||||
SQL
|
|
||||||
|
|
||||||
remove_index :post_tags, :post_id
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
class AddForeignKeyOnPostIdAndTagIdInPostTagSections < ActiveRecord::Migration[8.0]
|
|
||||||
def change
|
|
||||||
remove_foreign_key :post_tag_sections, :posts, column: :post_id
|
|
||||||
remove_foreign_key :post_tag_sections, :tags, column: :tag_id
|
|
||||||
|
|
||||||
add_foreign_key :post_tag_sections, :post_tags,
|
|
||||||
column: [:post_id, :tag_id],
|
|
||||||
primary_key: [:post_id, :tag_id],
|
|
||||||
on_delete: :cascade
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
class DeleteDiscardedRecordsFromTags < ActiveRecord::Migration[8.0]
|
|
||||||
def up
|
|
||||||
remove_foreign_key :tag_versions, :tags, column: :tag_id
|
|
||||||
remove_foreign_key :nico_tag_versions, :tags, column: :tag_id
|
|
||||||
remove_foreign_key :material_versions, :tags, column: :tag_id
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
DELETE
|
|
||||||
ntr
|
|
||||||
FROM
|
|
||||||
nico_tag_relations ntr
|
|
||||||
INNER JOIN
|
|
||||||
tags t
|
|
||||||
ON
|
|
||||||
t.discarded_at IS NOT NULL
|
|
||||||
AND t.id IN (ntr.tag_id, ntr.nico_tag_id)
|
|
||||||
SQL
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
DELETE
|
|
||||||
ti
|
|
||||||
FROM
|
|
||||||
tag_implications ti
|
|
||||||
INNER JOIN
|
|
||||||
tags t
|
|
||||||
ON
|
|
||||||
t.discarded_at IS NOT NULL
|
|
||||||
AND t.id IN (ti.tag_id, ti.parent_tag_id)
|
|
||||||
SQL
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
DELETE
|
|
||||||
FROM
|
|
||||||
tags
|
|
||||||
WHERE
|
|
||||||
discarded_at IS NOT NULL
|
|
||||||
SQL
|
|
||||||
|
|
||||||
remove_index :tags, :discarded_at
|
|
||||||
remove_column :tags, :discarded_at
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
class DeleteDiscardedRecordsFromTagNames < ActiveRecord::Migration[8.0]
|
|
||||||
def up
|
|
||||||
execute <<~SQL
|
|
||||||
DELETE
|
|
||||||
FROM
|
|
||||||
tag_names
|
|
||||||
WHERE
|
|
||||||
discarded_at IS NOT NULL
|
|
||||||
SQL
|
|
||||||
|
|
||||||
remove_index :tag_names, :discarded_at
|
|
||||||
remove_column :tag_names, :discarded_at
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
生成ファイル
+56
-30
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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|
|
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,6 +72,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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|
|
||||||
@@ -274,9 +275,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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"
|
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"
|
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.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|
|
||||||
@@ -285,13 +287,14 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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"
|
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"
|
t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -308,12 +311,23 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
|
|||||||
t.check_constraint "`begin_ms` >= 0", name: "chk_post_tag_sections_begin_ms_natural"
|
t.check_constraint "`begin_ms` >= 0", name: "chk_post_tag_sections_begin_ms_natural"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "post_tags", primary_key: ["post_id", "tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "post_tags", 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 "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "created_user_id"
|
t.bigint "created_user_id"
|
||||||
|
t.bigint "deleted_user_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.datetime "discarded_at"
|
||||||
|
t.virtual "is_active", type: :boolean, as: "(`discarded_at` is null)", stored: true
|
||||||
|
t.virtual "active_unique_key", type: :string, as: "(case when (`discarded_at` is null) then concat(`post_id`,_utf8mb4':',`tag_id`) else NULL end)", stored: true
|
||||||
|
t.index ["active_unique_key"], name: "idx_post_tags_active_unique", unique: true
|
||||||
t.index ["created_user_id"], name: "index_post_tags_on_created_user_id"
|
t.index ["created_user_id"], name: "index_post_tags_on_created_user_id"
|
||||||
|
t.index ["deleted_user_id"], name: "index_post_tags_on_deleted_user_id"
|
||||||
|
t.index ["discarded_at"], name: "index_post_tags_on_discarded_at"
|
||||||
|
t.index ["post_id", "discarded_at"], name: "index_post_tags_on_post_id_and_discarded_at"
|
||||||
|
t.index ["post_id"], name: "index_post_tags_on_post_id"
|
||||||
|
t.index ["tag_id", "discarded_at"], name: "index_post_tags_on_tag_id_and_discarded_at"
|
||||||
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -335,7 +349,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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"
|
||||||
@@ -347,7 +360,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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 "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
|
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|
|
||||||
@@ -365,6 +379,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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|
|
||||||
@@ -372,18 +387,25 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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
|
||||||
@@ -409,7 +431,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
|
|||||||
t.bigint "canonical_id"
|
t.bigint "canonical_id"
|
||||||
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.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
|
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
|
||||||
|
t.index ["discarded_at"], name: "index_tag_names_on_discarded_at"
|
||||||
t.index ["name"], name: "index_tag_names_on_name", unique: true
|
t.index ["name"], name: "index_tag_names_on_name", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -417,7 +441,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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"
|
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"
|
t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -433,9 +457,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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"
|
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"
|
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.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|
|
||||||
@@ -445,10 +470,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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.integer "post_count", default: 0, null: false
|
t.integer "post_count", default: 0, null: false
|
||||||
|
t.datetime "discarded_at"
|
||||||
t.integer "version_no", null: false
|
t.integer "version_no", null: false
|
||||||
t.index ["deprecated_at"], name: "index_tags_on_deprecated_at"
|
t.index ["deprecated_at"], name: "index_tags_on_deprecated_at"
|
||||||
|
t.index ["discarded_at"], name: "index_tags_on_discarded_at"
|
||||||
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
t.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|
|
||||||
@@ -586,19 +614,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
|
|||||||
t.index ["banned_at"], name: "index_users_on_banned_at"
|
t.index ["banned_at"], name: "index_users_on_banned_at"
|
||||||
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 "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "sha256", limit: 64, null: false
|
t.string "sha256", limit: 64, null: false
|
||||||
t.text "body", null: false
|
t.text "body", null: false
|
||||||
@@ -615,11 +630,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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|
|
||||||
@@ -664,6 +681,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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"
|
||||||
@@ -688,6 +707,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
|
|||||||
add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id"
|
add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "material_versions", "materials"
|
add_foreign_key "material_versions", "materials"
|
||||||
add_foreign_key "material_versions", "materials", column: "parent_id"
|
add_foreign_key "material_versions", "materials", column: "parent_id"
|
||||||
|
add_foreign_key "material_versions", "tags"
|
||||||
add_foreign_key "material_versions", "users", column: "created_by_user_id"
|
add_foreign_key "material_versions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "material_versions", "users", column: "updated_by_user_id"
|
add_foreign_key "material_versions", "users", column: "updated_by_user_id"
|
||||||
add_foreign_key "materials", "materials", column: "parent_id"
|
add_foreign_key "materials", "materials", column: "parent_id"
|
||||||
@@ -696,15 +716,18 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
|
|||||||
add_foreign_key "materials", "users", column: "updated_by_user_id"
|
add_foreign_key "materials", "users", column: "updated_by_user_id"
|
||||||
add_foreign_key "nico_tag_relations", "tags"
|
add_foreign_key "nico_tag_relations", "tags"
|
||||||
add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id"
|
add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id"
|
||||||
|
add_foreign_key "nico_tag_versions", "tags"
|
||||||
add_foreign_key "nico_tag_versions", "users", column: "created_by_user_id"
|
add_foreign_key "nico_tag_versions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "post_implications", "posts"
|
add_foreign_key "post_implications", "posts"
|
||||||
add_foreign_key "post_implications", "posts", column: "parent_post_id"
|
add_foreign_key "post_implications", "posts", column: "parent_post_id"
|
||||||
add_foreign_key "post_similarities", "posts"
|
add_foreign_key "post_similarities", "posts"
|
||||||
add_foreign_key "post_similarities", "posts", column: "target_post_id"
|
add_foreign_key "post_similarities", "posts", column: "target_post_id"
|
||||||
add_foreign_key "post_tag_sections", "post_tags", column: ["post_id", "tag_id"], primary_key: ["post_id", "tag_id"], on_delete: :cascade
|
add_foreign_key "post_tag_sections", "posts"
|
||||||
|
add_foreign_key "post_tag_sections", "tags"
|
||||||
add_foreign_key "post_tags", "posts"
|
add_foreign_key "post_tags", "posts"
|
||||||
add_foreign_key "post_tags", "tags"
|
add_foreign_key "post_tags", "tags"
|
||||||
add_foreign_key "post_tags", "users", column: "created_user_id"
|
add_foreign_key "post_tags", "users", column: "created_user_id"
|
||||||
|
add_foreign_key "post_tags", "users", column: "deleted_user_id"
|
||||||
add_foreign_key "post_versions", "posts"
|
add_foreign_key "post_versions", "posts"
|
||||||
add_foreign_key "post_versions", "users", column: "created_by_user_id"
|
add_foreign_key "post_versions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||||
@@ -714,6 +737,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do
|
|||||||
add_foreign_key "tag_names", "tag_names", column: "canonical_id"
|
add_foreign_key "tag_names", "tag_names", column: "canonical_id"
|
||||||
add_foreign_key "tag_similarities", "tags"
|
add_foreign_key "tag_similarities", "tags"
|
||||||
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
||||||
|
add_foreign_key "tag_versions", "tags"
|
||||||
add_foreign_key "tag_versions", "users", column: "created_by_user_id"
|
add_foreign_key "tag_versions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "tags", "tag_names"
|
add_foreign_key "tags", "tag_names"
|
||||||
add_foreign_key "theatre_comments", "theatres"
|
add_foreign_key "theatre_comments", "theatres"
|
||||||
@@ -740,6 +764,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) 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"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace :nico do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sync_post_tags! post, desired_tag_ids, current_tag_ids: nil
|
def sync_post_tags! post, desired_tag_ids, current_tag_ids: nil
|
||||||
current_tag_ids ||= PostTag.where(post_id: post.id).pluck(:tag_id).to_set
|
current_tag_ids ||= PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
|
||||||
desired_tag_ids = desired_tag_ids.compact.to_set
|
desired_tag_ids = desired_tag_ids.compact.to_set
|
||||||
|
|
||||||
to_add = desired_tag_ids - current_tag_ids
|
to_add = desired_tag_ids - current_tag_ids
|
||||||
@@ -30,8 +30,8 @@ namespace :nico do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each do |pt|
|
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||||
pt.destroy!
|
pt.discard_by!(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
require_relative '../../db/migrate/20260921020000_delete_discarded_records_from_tags'
|
|
||||||
require_relative '../../db/migrate/20260921030000_delete_discarded_records_from_tag_names'
|
|
||||||
|
|
||||||
RSpec.describe 'discarded tag cleanup migrations' do
|
|
||||||
[DeleteDiscardedRecordsFromTags, DeleteDiscardedRecordsFromTagNames].each do |migration_class|
|
|
||||||
it "rejects rollback of #{ migration_class.name }" do
|
|
||||||
expect { migration_class.new.down }
|
|
||||||
.to raise_error(ActiveRecord::IrreversibleMigration)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with legacy records' do
|
|
||||||
self.use_transactional_tests = false
|
|
||||||
|
|
||||||
before do
|
|
||||||
record_class = Class.new(ActiveRecord::Base) do
|
|
||||||
self.abstract_class = true
|
|
||||||
end
|
|
||||||
stub_const('TagCleanupMigrationRecord', record_class)
|
|
||||||
config = ActiveRecord::Base.connection_db_config.configuration_hash
|
|
||||||
@database = "btrc_hub_test_tag_cleanup_#{ Process.pid }_#{ SecureRandom.hex(4) }"
|
|
||||||
record_class.establish_connection(config.merge(database: nil))
|
|
||||||
@connection = record_class.lease_connection
|
|
||||||
@connection.create_database(@database)
|
|
||||||
@database_created = true
|
|
||||||
@connection.execute("USE #{ @connection.quote_table_name(@database) }")
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
@connection.drop_database(@database) if @database_created
|
|
||||||
ensure
|
|
||||||
TagCleanupMigrationRecord.remove_connection
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
@connection.create_table(:tag_names) do |t|
|
|
||||||
t.string :name, null: false, index: { unique: true }
|
|
||||||
t.bigint :canonical_id
|
|
||||||
t.datetime :discarded_at, index: true
|
|
||||||
end
|
|
||||||
@connection.add_foreign_key(:tag_names, :tag_names, column: :canonical_id)
|
|
||||||
@connection.create_table(:tags) do |t|
|
|
||||||
t.references :tag_name, null: false, foreign_key: true, index: { unique: true }
|
|
||||||
t.datetime :discarded_at, index: true
|
|
||||||
end
|
|
||||||
@connection.create_table(:nico_tag_relations) do |t|
|
|
||||||
t.references :tag, null: false, foreign_key: true
|
|
||||||
t.references :nico_tag, null: false, foreign_key: { to_table: :tags }
|
|
||||||
end
|
|
||||||
@connection.create_table(:tag_implications) do |t|
|
|
||||||
t.references :tag, null: false, foreign_key: true
|
|
||||||
t.references :parent_tag, null: false, foreign_key: { to_table: :tags }
|
|
||||||
end
|
|
||||||
[:tag_versions, :nico_tag_versions, :material_versions].each do |table|
|
|
||||||
@connection.create_table(table) do |t|
|
|
||||||
t.references :tag, null: false, foreign_key: true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@connection.execute(<<~SQL)
|
|
||||||
INSERT INTO tag_names (id, name, canonical_id, discarded_at) VALUES
|
|
||||||
(1, 'kept', NULL, NULL),
|
|
||||||
(2, 'merged_alias', 1, NULL),
|
|
||||||
(3, 'nico:deleted', NULL, '2026-09-20'),
|
|
||||||
(4, 'nico:kept', NULL, NULL),
|
|
||||||
(5, 'deleted_name', NULL, '2026-09-20')
|
|
||||||
SQL
|
|
||||||
@connection.execute(<<~SQL)
|
|
||||||
INSERT INTO tags (id, tag_name_id, discarded_at) VALUES
|
|
||||||
(1, 1, NULL), (2, 2, '2026-09-20'),
|
|
||||||
(3, 3, '2026-09-20'), (4, 4, NULL)
|
|
||||||
SQL
|
|
||||||
@connection.execute(<<~SQL)
|
|
||||||
INSERT INTO nico_tag_relations (id, tag_id, nico_tag_id) VALUES
|
|
||||||
(1, 1, 4), (2, 2, 4), (3, 1, 3), (4, 2, 3)
|
|
||||||
SQL
|
|
||||||
@connection.execute(<<~SQL)
|
|
||||||
INSERT INTO tag_implications (id, tag_id, parent_tag_id) VALUES
|
|
||||||
(1, 1, 4), (2, 2, 1), (3, 1, 2), (4, 2, 3)
|
|
||||||
SQL
|
|
||||||
@connection.execute('INSERT INTO tag_versions (tag_id) VALUES (1), (2)')
|
|
||||||
@connection.execute('INSERT INTO nico_tag_versions (tag_id) VALUES (3), (4)')
|
|
||||||
@connection.execute('INSERT INTO material_versions (tag_id) VALUES (1), (2)')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'removes discarded records and their links while retaining aliases and history' do
|
|
||||||
[DeleteDiscardedRecordsFromTags, DeleteDiscardedRecordsFromTagNames].each do |klass|
|
|
||||||
migration = klass.new
|
|
||||||
allow(migration).to receive(:connection).and_return(@connection)
|
|
||||||
migration.suppress_messages { migration.up }
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(@connection.select_values('SELECT id FROM tags ORDER BY id')).to eq([1, 4])
|
|
||||||
expect(@connection.select_rows('SELECT id, canonical_id FROM tag_names ORDER BY id'))
|
|
||||||
.to eq([[1, nil], [2, 1], [4, nil]])
|
|
||||||
expect(@connection.select_values('SELECT id FROM nico_tag_relations')).to eq([1])
|
|
||||||
expect(@connection.select_values('SELECT id FROM tag_implications')).to eq([1])
|
|
||||||
expect(@connection.select_values('SELECT tag_id FROM tag_versions ORDER BY tag_id'))
|
|
||||||
.to eq([1, 2])
|
|
||||||
expect(@connection.select_values('SELECT tag_id FROM nico_tag_versions ORDER BY tag_id'))
|
|
||||||
.to eq([3, 4])
|
|
||||||
expect(@connection.select_values('SELECT tag_id FROM material_versions ORDER BY tag_id'))
|
|
||||||
.to eq([1, 2])
|
|
||||||
|
|
||||||
[:tags, :tag_names].each do |table|
|
|
||||||
expect(@connection.column_exists?(table, :discarded_at)).to be(false)
|
|
||||||
expect(@connection.index_exists?(table, :discarded_at)).to be(false)
|
|
||||||
end
|
|
||||||
[:tag_versions, :nico_tag_versions, :material_versions].each do |table|
|
|
||||||
expect(@connection.foreign_key_exists?(table, :tags, column: :tag_id)).to be(false)
|
|
||||||
end
|
|
||||||
expect(@connection.foreign_key_exists?(:tags, :tag_names)).to be(true)
|
|
||||||
expect(@connection.foreign_key_exists?(:tag_names, :tag_names, column: :canonical_id))
|
|
||||||
.to be(true)
|
|
||||||
expect(@connection.index_exists?(:tag_names, :name, unique: true)).to be(true)
|
|
||||||
expect(@connection.index_exists?(:tags, :tag_name_id, unique: true)).to be(true)
|
|
||||||
[:nico_tag_relations, :tag_implications].each do |table|
|
|
||||||
expect(@connection.foreign_keys(table).map(&:to_table)).to eq(['tags', 'tags'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,73 +1,5 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe PostTag, type: :model do
|
RSpec.describe PostTag, type: :model do
|
||||||
describe 'uniqueness' do
|
|
||||||
it 'rejects duplicate post and tag pairs but allows either to be reused' do
|
|
||||||
post_tag = create(:post_tag)
|
|
||||||
duplicate = build(:post_tag, post: post_tag.post, tag: post_tag.tag)
|
|
||||||
|
|
||||||
expect(duplicate).not_to be_valid
|
|
||||||
expect(duplicate.errors.of_kind?(:post_id, :taken)).to be(true)
|
|
||||||
expect(build(:post_tag, post: post_tag.post, tag: create(:tag))).to be_valid
|
|
||||||
expect(build(:post_tag, post: create(:post), tag: post_tag.tag)).to be_valid
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'enforces uniqueness in the database when validation is bypassed' do
|
|
||||||
post_tag = create(:post_tag)
|
|
||||||
duplicate = build(:post_tag, post: post_tag.post, tag: post_tag.tag)
|
|
||||||
|
|
||||||
expect { duplicate.save!(validate: false) }
|
|
||||||
.to raise_error(ActiveRecord::RecordNotUnique)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#destroy!' do
|
|
||||||
it 'deletes only the selected pair and its sections and updates the counter' do
|
|
||||||
post_tag = create(:post_tag)
|
|
||||||
same_post = create(:post_tag, post: post_tag.post)
|
|
||||||
same_tag = create(:post_tag, tag: post_tag.tag)
|
|
||||||
sections = [post_tag, same_post, same_tag].map do |link|
|
|
||||||
create(:post_tag_section, post: link.post, tag: link.tag,
|
|
||||||
begin_ms: 1000, end_ms: 2000)
|
|
||||||
end
|
|
||||||
|
|
||||||
expect { post_tag.destroy! }.to change(described_class, :count).by(-1)
|
|
||||||
.and change(PostTagSection, :count).by(-1)
|
|
||||||
.and change { post_tag.tag.reload.post_count }.from(2).to(1)
|
|
||||||
|
|
||||||
expect(described_class.exists?(post: post_tag.post, tag: post_tag.tag)).to be(false)
|
|
||||||
expect(same_post.reload).to be_persisted
|
|
||||||
expect(same_tag.reload).to be_persisted
|
|
||||||
expect(PostTagSection.all).to contain_exactly(*sections.drop(1))
|
|
||||||
expect(post_tag.post.reload.tags).to contain_exactly(same_post.tag)
|
|
||||||
expect(post_tag.tag.reload.posts).to contain_exactly(same_tag.post)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows a removed tag to be added again without restoring old sections' do
|
|
||||||
post_tag = create(:post_tag)
|
|
||||||
create(:post_tag_section, post: post_tag.post, tag: post_tag.tag,
|
|
||||||
begin_ms: 1000, end_ms: 2000)
|
|
||||||
post_tag.destroy!
|
|
||||||
|
|
||||||
replacement = create(:post_tag, post: post_tag.post, tag: post_tag.tag)
|
|
||||||
|
|
||||||
expect(replacement.reload.sections).to be_empty
|
|
||||||
expect(replacement.tag.reload.post_count).to eq(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#sections' do
|
describe '#sections' do
|
||||||
it 'loads the owning post_tag from a section using both keys' do
|
|
||||||
post_tag = create(:post_tag)
|
|
||||||
create(:post_tag, post: post_tag.post)
|
|
||||||
create(:post_tag, tag: post_tag.tag)
|
|
||||||
section = create(:post_tag_section, post: post_tag.post,
|
|
||||||
tag: post_tag.tag,
|
|
||||||
begin_ms: 1000, end_ms: 2000)
|
|
||||||
|
|
||||||
expect(section.reload.post_tag).to eq(post_tag)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'loads sections by post_id and tag_id' do
|
it 'loads sections by post_id and tag_id' do
|
||||||
post_tag = create(:post_tag)
|
post_tag = create(:post_tag)
|
||||||
section = create(:post_tag_section,
|
section = create(:post_tag_section,
|
||||||
@@ -80,25 +12,18 @@ RSpec.describe PostTag, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'does not load sections for another tag on the same post' do
|
it 'does not load sections for another tag on the same post' do
|
||||||
post_tag = create(:post_tag)
|
post = create(:post)
|
||||||
post = post_tag.post
|
tag = create(:tag)
|
||||||
other_tag = create(:tag)
|
other_tag = create(:tag)
|
||||||
|
|
||||||
own_section = create(:post_tag_section,
|
post_tag = create(:post_tag, post:, tag:)
|
||||||
post:,
|
|
||||||
tag: post_tag.tag,
|
|
||||||
begin_ms: 1000,
|
|
||||||
end_ms: 2000)
|
|
||||||
|
|
||||||
create(:post_tag, post:, tag: other_tag)
|
|
||||||
|
|
||||||
create(:post_tag_section,
|
create(:post_tag_section,
|
||||||
post:,
|
post:,
|
||||||
tag: other_tag,
|
tag: other_tag,
|
||||||
begin_ms: 1000,
|
begin_ms: 1000,
|
||||||
end_ms: 2000)
|
end_ms: 2000)
|
||||||
|
|
||||||
expect(post_tag.reload.sections).to contain_exactly(own_section)
|
expect(post_tag.sections).to be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows open-ended sections' do
|
it 'allows open-ended sections' do
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
|||||||
|
|
||||||
it 'deletes the source tag_name' do
|
it 'deletes the source tag_name' do
|
||||||
described_class.apply!
|
described_class.apply!
|
||||||
expect(TagName.unscoped.exists?(source.id)).to be(false)
|
expect(TagName.exists?(source.id)).to be(false)
|
||||||
expect(existing.reload.name).to eq('foobar')
|
expect(existing.reload.name).to eq('foobar')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -75,27 +75,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
|||||||
described_class.apply!
|
described_class.apply!
|
||||||
expected_tag_name_id = existing.canonical_id || existing.id
|
expected_tag_name_id = existing.canonical_id || existing.id
|
||||||
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
|
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
|
||||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
expect(TagName.exists?(source_tag_name_id)).to be(false)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the sanitised name is an alias of an existing tag' do
|
|
||||||
let!(:existing_tag) { create(:tag) }
|
|
||||||
let!(:alias_name) do
|
|
||||||
TagName.create!(name: 'foobar', canonical: existing_tag.tag_name)
|
|
||||||
end
|
|
||||||
let!(:source) do
|
|
||||||
TagName.create!(name: 'tmp').tap do |tn|
|
|
||||||
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'deletes only the source and preserves the alias and its canonical tag' do
|
|
||||||
described_class.apply!
|
|
||||||
|
|
||||||
expect(TagName.unscoped.exists?(source.id)).to be(false)
|
|
||||||
expect(alias_name.reload.canonical).to eq(existing_tag.tag_name)
|
|
||||||
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -112,15 +92,13 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'merges the source tag into the existing tag and deletes the source tag_name' do
|
it 'merges the source tag into the existing tag and deletes the source tag_name' do
|
||||||
post = create(:post)
|
expect(TagName.find_by(name: 'foobar')&.tag&.id).to eq(existing_tag.id)
|
||||||
PostTag.create!(post:, tag: source_tag)
|
expect(TagName.find_by(name: 'foo_bar')&.tag&.id).to eq(source_tag.id)
|
||||||
|
|
||||||
described_class.apply!
|
described_class.apply!
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.exists?(source_tag.id)).to be(false)
|
||||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
expect(TagName.exists?(source_tag.tag_name_id)).to be(false)
|
||||||
expect(post.reload.tags).to contain_exactly(existing_tag)
|
|
||||||
expect(existing_tag.reload.name).to eq('foobar')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+48
-186
@@ -173,47 +173,6 @@ RSpec.describe Tag, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.find_or_create_by_tag_name!' do
|
|
||||||
it 'creates a tag and name with the requested category after stripping whitespace' do
|
|
||||||
tag = nil
|
|
||||||
|
|
||||||
expect {
|
|
||||||
tag = described_class.find_or_create_by_tag_name!(
|
|
||||||
' lookup_new ', category: :character)
|
|
||||||
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
|
|
||||||
|
|
||||||
expect(tag.name).to eq('lookup_new')
|
|
||||||
expect(tag.category).to eq('character')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'reuses the canonical tag for an alias without changing its category' do
|
|
||||||
tag = create(:tag, category: :character)
|
|
||||||
alias_name = TagName.create!(name: 'lookup_alias', canonical: tag.tag_name)
|
|
||||||
found = nil
|
|
||||||
|
|
||||||
expect {
|
|
||||||
found = described_class.find_or_create_by_tag_name!(
|
|
||||||
alias_name.name, category: :general)
|
|
||||||
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
|
|
||||||
|
|
||||||
expect(found).to eq(tag)
|
|
||||||
expect(found.category).to eq('character')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a tag for an existing canonical name reached through an alias' do
|
|
||||||
canonical = create(:tag_name)
|
|
||||||
alias_name = TagName.create!(name: 'lookup_alias', canonical:)
|
|
||||||
tag = nil
|
|
||||||
|
|
||||||
expect {
|
|
||||||
tag = described_class.find_or_create_by_tag_name!(
|
|
||||||
alias_name.name, category: :general)
|
|
||||||
}.to change(Tag, :count).by(1).and change(TagName, :count).by(0)
|
|
||||||
|
|
||||||
expect(tag.tag_name).to eq(canonical)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.merge_tags!' do
|
describe '.merge_tags!' do
|
||||||
let!(:target_tag) { create(:tag, category: :general) }
|
let!(:target_tag) { create(:tag, category: :general) }
|
||||||
let!(:source_tag) { create(:tag, category: :general) }
|
let!(:source_tag) { create(:tag, category: :general) }
|
||||||
@@ -226,14 +185,18 @@ RSpec.describe Tag, type: :model do
|
|||||||
context 'when merging a simple source tag' do
|
context 'when merging a simple source tag' do
|
||||||
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
||||||
|
|
||||||
it 'deletes the source tag, moves its post link, and keeps its name as an alias' do
|
it 'discards the source post_tag, creates an active target post_tag, discards the source tag, and aliases the source tag_name' do
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
described_class.merge_tags!(target_tag, [source_tag])
|
||||||
|
|
||||||
target_link = PostTag.find_by(post: post_record, tag: target_tag)
|
source_pt = PostTag.with_discarded.find(source_post_tag.id)
|
||||||
|
active_target = PostTag.kept.find_by(post_id: post_record.id, tag_id: target_tag.id)
|
||||||
|
|
||||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
expect(source_pt.discarded_at).to be_present
|
||||||
expect(target_link).to be_present
|
expect(source_pt.tag_id).to eq(source_tag.id)
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(active_target).to be_present
|
||||||
|
|
||||||
|
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
|
||||||
|
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
|
||||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
@@ -243,101 +206,38 @@ RSpec.describe Tag, type: :model do
|
|||||||
let!(:target_post_tag) { PostTag.create!(post: post_record, tag: target_tag) }
|
let!(:target_post_tag) { PostTag.create!(post: post_record, tag: target_tag) }
|
||||||
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
||||||
|
|
||||||
it 'deletes the source link and preserves the existing target link' do
|
it 'discards the source post_tag, keeps one active target post_tag, discards the source tag, and aliases the source tag_name' do
|
||||||
create(:post_tag_section, post: post_record, tag: source_tag,
|
|
||||||
begin_ms: 1000, end_ms: 2000)
|
|
||||||
target_section = create(:post_tag_section, post: post_record,
|
|
||||||
tag: target_tag,
|
|
||||||
begin_ms: 3000, end_ms: nil)
|
|
||||||
|
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
described_class.merge_tags!(target_tag, [source_tag])
|
||||||
|
|
||||||
target_links = PostTag.where(post: post_record, tag: target_tag)
|
source_pt = PostTag.with_discarded.find(source_post_tag.id)
|
||||||
|
active = PostTag.kept.where(post_id: post_record.id, tag_id: target_tag.id)
|
||||||
|
|
||||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
expect(source_pt.discarded_at).to be_present
|
||||||
expect(target_links).to contain_exactly(target_post_tag)
|
expect(source_pt.tag_id).to eq(source_tag.id)
|
||||||
expect(PostTagSection.where(post: post_record, tag: source_tag)).to be_empty
|
expect(active.count).to eq(1)
|
||||||
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
|
expect(active.first.id).to eq(target_post_tag.id)
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
|
||||||
|
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
|
||||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps source history and records the new target alias after deleting the source' do
|
|
||||||
user = create_member_user!
|
|
||||||
source_name = source_tag.name
|
|
||||||
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name)
|
|
||||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
|
|
||||||
original_version = source_tag.tag_versions.first
|
|
||||||
|
|
||||||
described_class.merge_tags!(target_tag, [source_tag], created_by_user: user)
|
|
||||||
|
|
||||||
versions = TagVersion.where(tag_id: source_tag.id).order(:version_no)
|
|
||||||
expect(versions.pluck(:version_no, :event_type))
|
|
||||||
.to eq([[1, 'create'], [2, 'discard']])
|
|
||||||
expect(versions.first).to eq(original_version)
|
|
||||||
expect(versions.last).to have_attributes(
|
|
||||||
name: source_name, aliases: source_alias.name, created_by_user: user)
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
|
||||||
|
|
||||||
target_versions = target_tag.tag_versions.order(:version_no)
|
|
||||||
expect(target_versions.pluck(:event_type)).to eq(['create', 'update'])
|
|
||||||
expect(target_versions.last.aliases.split).to eq([source_name])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'deletes source relationships while preserving unrelated relationships' do
|
|
||||||
parent = create(:tag)
|
|
||||||
child = create(:tag)
|
|
||||||
nico_tag = create(:tag, :nico)
|
|
||||||
TagImplication.create!(tag: source_tag, parent_tag: parent)
|
|
||||||
TagImplication.create!(tag: child, parent_tag: source_tag)
|
|
||||||
kept_implication = TagImplication.create!(tag: target_tag, parent_tag: parent)
|
|
||||||
NicoTagRelation.create!(tag: source_tag, nico_tag:)
|
|
||||||
kept_relation = NicoTagRelation.create!(tag: target_tag, nico_tag:)
|
|
||||||
TagSimilarity.create!(tag: source_tag, target_tag:, cos: 0.5)
|
|
||||||
TagSimilarity.create!(tag: target_tag, target_tag: source_tag, cos: 0.5)
|
|
||||||
kept_similarity = TagSimilarity.create!(tag: target_tag, target_tag: parent,
|
|
||||||
cos: 0.5)
|
|
||||||
|
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
|
||||||
|
|
||||||
expect(TagImplication.all).to contain_exactly(kept_implication)
|
|
||||||
expect(NicoTagRelation.all).to contain_exactly(kept_relation)
|
|
||||||
expect(TagSimilarity.all).to contain_exactly(kept_similarity)
|
|
||||||
expect(TagVersion.where(tag_id: source_tag.id).order(:version_no).last.parent_tag_ids)
|
|
||||||
.to eq(parent.id.to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'preserves material history referencing the deleted source tag' do
|
|
||||||
source_tag.update!(category: :material)
|
|
||||||
target_tag.update!(category: :material)
|
|
||||||
material = Material.create!(tag: source_tag, url: 'https://example.com/material')
|
|
||||||
version = MaterialVersionRecorder.record!(
|
|
||||||
material:, event_type: :create, created_by_user: nil)
|
|
||||||
material.update!(tag: target_tag)
|
|
||||||
|
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
|
||||||
|
|
||||||
expect(version.reload).to have_attributes(
|
|
||||||
tag_id: source_tag.id, tag_name: source_tag_name.name, tag_category: 'material')
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
|
||||||
expect(material.reload.tag).to eq(target_tag)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when source_tags includes the target itself' do
|
context 'when source_tags includes the target itself' do
|
||||||
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
||||||
|
|
||||||
it 'ignores the target in source_tags while still merging the source tag' do
|
it 'ignores the target in source_tags while still merging the source tag' do
|
||||||
described_class.merge_tags!(target_tag, [source_tag, target_tag])
|
described_class.merge_tags!(target_tag, [source_tag, target_tag])
|
||||||
|
|
||||||
target_link = PostTag.find_by(post: post_record, tag: target_tag)
|
source_pt = PostTag.with_discarded.find(source_post_tag.id)
|
||||||
|
active_target = PostTag.kept.find_by(post_id: post_record.id, tag_id: target_tag.id)
|
||||||
|
|
||||||
expect(Tag.find(target_tag.id)).to be_present
|
expect(Tag.find(target_tag.id)).to be_present
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
|
||||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
expect(source_pt.discarded_at).to be_present
|
||||||
expect(target_link).to be_present
|
expect(source_pt.tag_id).to eq(source_tag.id)
|
||||||
|
expect(active_target).to be_present
|
||||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
@@ -360,16 +260,18 @@ RSpec.describe Tag, type: :model do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'still merges and keeps the source name as an alias without validating it' do
|
it 'still merges, but discards the source tag_name instead of aliasing it' do
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
described_class.merge_tags!(target_tag, [source_tag])
|
||||||
|
|
||||||
target_link = PostTag.find_by(post: post_record, tag: target_tag)
|
source_pt = PostTag.with_discarded.find(source_post_tag.id)
|
||||||
|
active_target = PostTag.kept.find_by(post_id: post_record.id, tag_id: target_tag.id)
|
||||||
|
discarded_source_tag_name = TagName.with_discarded.find(source_tag_name.id)
|
||||||
|
|
||||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
expect(source_pt.discarded_at).to be_present
|
||||||
expect(target_link).to be_present
|
expect(source_pt.tag_id).to eq(source_tag.id)
|
||||||
|
expect(active_target).to be_present
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
|
||||||
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -386,72 +288,36 @@ RSpec.describe Tag, type: :model do
|
|||||||
message: 'init')
|
message: 'init')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do
|
it 'rolls back the transaction' do
|
||||||
earlier_source = create(:tag)
|
|
||||||
earlier_name = earlier_source.tag_name
|
|
||||||
source_section = create(:post_tag_section, post: post_record,
|
|
||||||
tag: source_tag,
|
|
||||||
begin_ms: 1000, end_ms: 2000)
|
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
described_class.merge_tags!(target_tag, [earlier_source, source_tag])
|
described_class.merge_tags!(target_tag, [source_tag])
|
||||||
}.to raise_error(ActiveRecord::RecordInvalid)
|
}.to raise_error(ActiveRecord::RecordInvalid)
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
|
expect(Tag.with_discarded.find(source_tag.id)).not_to be_discarded
|
||||||
expect(earlier_name.reload.canonical_id).to be_nil
|
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
|
||||||
expect(TagVersion.where(tag_id: [earlier_source.id, source_tag.id, target_tag.id]))
|
expect(PostTag.kept.find(source_post_tag.id).tag_id).to eq(source_tag.id)
|
||||||
.to be_empty
|
expect(PostTag.kept.find_by(post_id: post_record.id, tag_id: target_tag.id)).to be_nil
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(true)
|
|
||||||
expect(TagName.unscoped.exists?(source_tag_name.id)).to be(true)
|
|
||||||
expect(source_post_tag.reload.tag_id).to eq(source_tag.id)
|
|
||||||
expect(source_post_tag.sections).to contain_exactly(source_section)
|
|
||||||
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
|
|
||||||
expect(source_tag.reload.post_count).to eq(1)
|
|
||||||
expect(source_tag_name.reload.canonical_id).to be_nil
|
expect(source_tag_name.reload.canonical_id).to be_nil
|
||||||
expect(target_tag.reload.post_count).to eq(0)
|
expect(target_tag.reload.post_count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when merging a nico source tag' do
|
context 'when merging a nico source tag' do
|
||||||
let!(:target_tag) do
|
let!(:target_tag) { create(:tag, category: :nico, name: 'nico:foo') }
|
||||||
create(:tag, category: :nico, tag_name: create(:tag_name, name: 'nico:foo'))
|
let!(:source_tag) { create(:tag, category: :nico, name: 'nico:bar') }
|
||||||
end
|
|
||||||
let!(:source_tag) do
|
|
||||||
create(:tag, category: :nico, tag_name: create(:tag_name, name: 'nico:bar'))
|
|
||||||
end
|
|
||||||
let!(:source_tag_name_id) { source_tag.tag_name_id }
|
let!(:source_tag_name_id) { source_tag.tag_name_id }
|
||||||
|
|
||||||
it 'deletes the source tag and name instead of keeping an alias' do
|
it 'discards the source tag_name instead of aliasing it' do
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
described_class.merge_tags!(target_tag, [source_tag])
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
discarded_source_tag = Tag.with_discarded.find(source_tag.id)
|
||||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
discarded_source_tag_name = TagName.with_discarded.find(source_tag_name_id)
|
||||||
|
|
||||||
|
expect(discarded_source_tag).to be_discarded
|
||||||
|
expect(discarded_source_tag_name).to be_discarded
|
||||||
|
expect(discarded_source_tag_name.canonical_id).to be_nil
|
||||||
expect(target_tag.reload.post_count).to eq(0)
|
expect(target_tag.reload.post_count).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps nico history while deleting source links and allows recreating the name' do
|
|
||||||
linked_tag = create(:tag)
|
|
||||||
NicoTagRelation.create!(nico_tag: source_tag, tag: linked_tag)
|
|
||||||
kept_relation = NicoTagRelation.create!(nico_tag: target_tag, tag: linked_tag)
|
|
||||||
user = create_member_user!
|
|
||||||
source_name = source_tag.name
|
|
||||||
|
|
||||||
described_class.merge_tags!(target_tag, [source_tag], created_by_user: user)
|
|
||||||
|
|
||||||
expect(NicoTagRelation.all).to contain_exactly(kept_relation)
|
|
||||||
versions = NicoTagVersion.where(tag_id: source_tag.id).order(:version_no)
|
|
||||||
expect(versions.pluck(:version_no, :event_type))
|
|
||||||
.to eq([[1, 'create'], [2, 'discard']])
|
|
||||||
expect(versions.last).to have_attributes(
|
|
||||||
name: source_name, linked_tags: linked_tag.name, created_by_user: user)
|
|
||||||
|
|
||||||
recreated = described_class.find_or_create_by_tag_name!(source_name, category: :nico)
|
|
||||||
|
|
||||||
expect(recreated.id).not_to eq(source_tag.id)
|
|
||||||
expect(recreated.tag_name_id).not_to eq(source_tag_name_id)
|
|
||||||
expect(recreated.nico_tag_versions).to be_empty
|
|
||||||
expect(versions.reload.size).to eq(2)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def snapshot_tags(post)
|
def snapshot_tags(post)
|
||||||
@@ -467,7 +333,6 @@ 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,
|
||||||
@@ -499,15 +364,12 @@ RSpec.describe Tag, type: :model do
|
|||||||
expect(latest.event_type).to eq('update')
|
expect(latest.event_type).to eq('update')
|
||||||
expect(latest.created_by_user).to be_nil
|
expect(latest.created_by_user).to be_nil
|
||||||
expect(latest.tags).to eq(snapshot_tags(post_record.reload))
|
expect(latest.tags).to eq(snapshot_tags(post_record.reload))
|
||||||
expect(latest.tags_json.map { |item| item.fetch('id') }).to eq([target_tag.id])
|
|
||||||
expect(affected_versions.first.tags_json.map { |item| item.fetch('id') })
|
|
||||||
.to eq([source_tag.id])
|
|
||||||
|
|
||||||
expect(unaffected_post.reload.post_versions.count).to eq(1)
|
expect(unaffected_post.reload.post_versions.count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the source tag has no post_tags' do
|
context 'when the source tag has no active post_tags' do
|
||||||
let!(:another_post) do
|
let!(:another_post) do
|
||||||
Post.create!(url: 'https://example.com/posts/3', title: 'another post')
|
Post.create!(url: 'https://example.com/posts/3', title: 'another post')
|
||||||
end
|
end
|
||||||
|
|||||||
+116
-142
@@ -55,7 +55,6 @@ 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,
|
||||||
@@ -148,15 +147,12 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
it 'keeps children and sections keys in non-detail tag responses' do
|
it 'keeps children and sections keys in non-detail tag responses' do
|
||||||
PostTagSection.create!(post: hit_post, tag:, begin_ms: 1_000, end_ms: nil)
|
PostTagSection.create!(post: hit_post, tag:, begin_ms: 1_000, end_ms: nil)
|
||||||
deprecated_tag = create(:tag, deprecated_at: Time.current)
|
|
||||||
create(:post_tag, post: hit_post, tag: deprecated_tag)
|
|
||||||
|
|
||||||
get '/posts'
|
get '/posts'
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
hit_json = json.fetch('posts').find { |post| post['id'] == hit_post.id }
|
hit_json = json.fetch('posts').find { |post| post['id'] == hit_post.id }
|
||||||
expect(hit_json.fetch('tags').map { |item| item.fetch('id') }).to eq([tag.id])
|
|
||||||
tag_json = hit_json.fetch('tags').find { |item| item['name'] == 'spec_tag' }
|
tag_json = hit_json.fetch('tags').find { |item| item['name'] == 'spec_tag' }
|
||||||
|
|
||||||
expect(tag_json.fetch('children')).to eq([])
|
expect(tag_json.fetch('children')).to eq([])
|
||||||
@@ -165,26 +161,6 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'preloads tag details and sections as the number of posts grows' do
|
|
||||||
5.times do
|
|
||||||
link = create(:post_tag, post: create(:post, uploaded_user: user))
|
|
||||||
create(:post_tag_section, post: link.post, tag: link.tag,
|
|
||||||
begin_ms: 1000, end_ms: 2000)
|
|
||||||
end
|
|
||||||
get '/posts', params: { limit: 1 }
|
|
||||||
|
|
||||||
one_post_queries = count_sql_queries do
|
|
||||||
get '/posts', params: { limit: 1 }
|
|
||||||
end
|
|
||||||
many_post_queries = count_sql_queries do
|
|
||||||
get '/posts', params: { limit: 20 }
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.fetch('posts').size).to eq(8)
|
|
||||||
expect(many_post_queries).to be <= one_post_queries
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when q is provided' do
|
context 'when q is provided' do
|
||||||
it 'filters posts by q (hit case)' do
|
it 'filters posts by q (hit case)' do
|
||||||
get '/posts', params: { tags: 'spec_tag' }
|
get '/posts', params: { tags: 'spec_tag' }
|
||||||
@@ -483,71 +459,6 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when update times include version history' do
|
|
||||||
let(:t0) { Time.zone.parse('2020-01-01 12:00:00') }
|
|
||||||
let(:t1) { t0 + 1.day }
|
|
||||||
let(:t2) { t0 + 2.days }
|
|
||||||
let(:t3) { t0 + 3.days }
|
|
||||||
let!(:history_post) do
|
|
||||||
create(:post, url: 'https://example.com/version-time/history',
|
|
||||||
created_at: t0, updated_at: t0)
|
|
||||||
end
|
|
||||||
let!(:plain_post) do
|
|
||||||
create(:post, url: 'https://example.com/version-time/plain',
|
|
||||||
created_at: t1, updated_at: t1)
|
|
||||||
end
|
|
||||||
let!(:newer_post) do
|
|
||||||
create(:post, url: 'https://example.com/version-time/newer',
|
|
||||||
created_at: t0, updated_at: t3)
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
link = create(:post_tag, post: history_post, tag:)
|
|
||||||
travel_to(t0) do
|
|
||||||
PostVersionRecorder.record!(post: history_post,
|
|
||||||
event_type: :create, created_by_user: nil)
|
|
||||||
PostVersionRecorder.record!(post: newer_post,
|
|
||||||
event_type: :create, created_by_user: nil)
|
|
||||||
end
|
|
||||||
travel_to(t2) do
|
|
||||||
link.destroy!
|
|
||||||
PostVersionRecorder.record!(post: history_post,
|
|
||||||
event_type: :update, created_by_user: nil)
|
|
||||||
end
|
|
||||||
create(:post_tag, post: plain_post, tag:, created_at: t3)
|
|
||||||
end
|
|
||||||
|
|
||||||
['asc', 'desc'].each do |direction|
|
|
||||||
it "sorts by the later of post update and latest version time (#{ direction })" do
|
|
||||||
get '/posts', params: { url: '/version-time/', order: "updated_at:#{ direction }" }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expected_ids = [plain_post.id, history_post.id, newer_post.id]
|
|
||||||
expected_ids.reverse! if direction == 'desc'
|
|
||||||
expect(json.fetch('posts').map { |item| item.fetch('id') }).to eq(expected_ids)
|
|
||||||
expect(json.fetch('count')).to eq(3)
|
|
||||||
|
|
||||||
times = json.fetch('posts').to_h do |item|
|
|
||||||
[item.fetch('id'), Time.zone.parse(item.fetch('updated_at'))]
|
|
||||||
end
|
|
||||||
expect(times).to eq({ plain_post.id => t1,
|
|
||||||
history_post.id => t2,
|
|
||||||
newer_post.id => t3 })
|
|
||||||
expect(history_post.reload.updated_at).to eq(t0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'filters inclusively by the latest version time after a tag is deleted' do
|
|
||||||
get '/posts', params: { url: '/version-time/',
|
|
||||||
updated_from: t2.iso8601,
|
|
||||||
updated_to: t2.iso8601 }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.fetch('posts').map { |item| item.fetch('id') }).to eq([history_post.id])
|
|
||||||
expect(json.fetch('count')).to eq(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when original_created_from/original_created_to are provided' do
|
context 'when original_created_from/original_created_to are provided' do
|
||||||
# 注意: controller の現状ロジックに合わせてる
|
# 注意: controller の現状ロジックに合わせてる
|
||||||
# original_created_from は `original_created_before > ?`
|
# original_created_from は `original_created_before > ?`
|
||||||
@@ -1252,7 +1163,9 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
context 'when nico tag already exists in tags' do
|
context 'when nico tag already exists in tags' do
|
||||||
before do
|
before do
|
||||||
Tag.find_or_create_by_tag_name!('nico:nico_tag', category: :nico)
|
Tag.find_undiscard_or_create_by!(
|
||||||
|
tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
|
||||||
|
category: :nico)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 422 with tag field errors' do
|
it 'returns 422 with tag field errors' do
|
||||||
@@ -1505,11 +1418,9 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
it '200 and updates title + resync tags when member' do
|
it '200 and updates title + resync tags when member' do
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
create(:post_tag_section, post: post_record, tag:,
|
|
||||||
begin_ms: 1000, end_ms: 2000)
|
|
||||||
|
|
||||||
tn2 = TagName.create!(name: 'spec_tag_2')
|
tn2 = TagName.create!(name: 'spec_tag_2')
|
||||||
replacement_tag = Tag.create!(tag_name: tn2, category: :general)
|
Tag.create!(tag_name: tn2, category: :general)
|
||||||
|
|
||||||
put "/posts/#{post_record.id}", params: post_update_params(
|
put "/posts/#{post_record.id}", params: post_update_params(
|
||||||
post_record,
|
post_record,
|
||||||
@@ -1522,38 +1433,6 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
names = json['tags'].map { |n| n['name'] }
|
names = json['tags'].map { |n| n['name'] }
|
||||||
expect(names).to include('spec_tag_2')
|
expect(names).to include('spec_tag_2')
|
||||||
expect(names).not_to include('spec_tag')
|
|
||||||
expect(PostTag.exists?(post: post_record, tag:)).to be(false)
|
|
||||||
expect(PostTagSection.exists?(post: post_record, tag:)).to be(false)
|
|
||||||
expect(tag.reload.post_count).to eq(0)
|
|
||||||
expect(replacement_tag.reload.post_count).to eq(1)
|
|
||||||
|
|
||||||
versions = post_record.post_versions.order(:version_no)
|
|
||||||
expect(versions.first.tags_json).to include(
|
|
||||||
a_hash_including('id' => tag.id,
|
|
||||||
'sections' => [{ 'begin_ms' => 1000, 'end_ms' => 2000 }]))
|
|
||||||
expect(versions.last.tags_json.map { |item| item.fetch('id') })
|
|
||||||
.not_to include(tag.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'can add a removed tag again and records both changes' do
|
|
||||||
sign_in_as(member)
|
|
||||||
|
|
||||||
put "/posts/#{ post_record.id }", params: post_update_params(post_record, tags: '')
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(PostTag.exists?(post: post_record, tag:)).to be(false)
|
|
||||||
|
|
||||||
put "/posts/#{ post_record.id }", params: post_update_params(
|
|
||||||
post_record, tags: 'spec_tag')
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(PostTag.where(post: post_record, tag:).count).to eq(1)
|
|
||||||
expect(PostTag.find_by!(post: post_record, tag:).created_user).to eq(member)
|
|
||||||
expect(tag.reload.post_count).to eq(1)
|
|
||||||
snapshots = post_record.post_versions.order(:version_no).map do |version|
|
|
||||||
version.tags_json.map { |item| item.fetch('id') }
|
|
||||||
end
|
|
||||||
expect(snapshots.map { |ids| ids.include?(tag.id) }).to eq([true, false, true])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects a deprecated tag specified directly' do
|
it 'rejects a deprecated tag specified directly' do
|
||||||
@@ -1578,7 +1457,9 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
context 'when nico tag already exists in tags' do
|
context 'when nico tag already exists in tags' do
|
||||||
before do
|
before do
|
||||||
Tag.find_or_create_by_tag_name!('nico:nico_tag', category: :nico)
|
Tag.find_undiscard_or_create_by!(
|
||||||
|
tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
|
||||||
|
category: :nico)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 422 with tag field errors' do
|
it 'returns 422 with tag field errors' do
|
||||||
@@ -1997,29 +1878,123 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns viewed state and current tags with their sections' do
|
it '200 and returns viewed boolean' do
|
||||||
create(:post_tag_section, post: post_record, tag:,
|
|
||||||
begin_ms: 1000, end_ms: nil)
|
|
||||||
deprecated_tag = create(:tag, deprecated_at: Time.current)
|
|
||||||
create(:post_tag, post: post_record, tag: deprecated_tag)
|
|
||||||
|
|
||||||
get '/posts/random'
|
get '/posts/random'
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json).to have_key('viewed')
|
expect(json).to have_key('viewed')
|
||||||
expect([true, false]).to include(json['viewed'])
|
expect([true, false]).to include(json['viewed'])
|
||||||
expect(json.fetch('tags')).to contain_exactly(
|
|
||||||
a_hash_including('id' => tag.id,
|
|
||||||
'children' => [],
|
|
||||||
'sections' => [{ 'begin_ms' => 1000, 'end_ms' => nil }]))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /posts/changes' do
|
describe 'GET /posts/changes' do
|
||||||
it 'returns 404 for the retired history endpoint' do
|
let(:member) { create(:user, :member) }
|
||||||
get '/posts/changes'
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:not_found)
|
it 'returns add/remove events (history) for a post' do
|
||||||
|
# add
|
||||||
|
tn2 = TagName.create!(name: 'spec_tag2')
|
||||||
|
tag2 = Tag.create!(tag_name: tn2, category: :general)
|
||||||
|
pt = PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
||||||
|
|
||||||
|
# remove (discard)
|
||||||
|
pt.discard_by!(member)
|
||||||
|
|
||||||
|
get '/posts/changes', params: { id: post_record.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('changes', 'count')
|
||||||
|
expect(json['changes']).to be_an(Array)
|
||||||
|
expect(json['count']).to be >= 2
|
||||||
|
|
||||||
|
types = json['changes'].map { |e| e['change_type'] }.uniq
|
||||||
|
expect(types).to include('add')
|
||||||
|
expect(types).to include('remove')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters history by tag' do
|
||||||
|
tn2 = TagName.create!(name: 'history_tag_hit')
|
||||||
|
tag2 = Tag.create!(tag_name: tn2, category: :general)
|
||||||
|
|
||||||
|
tn3 = TagName.create!(name: 'history_tag_miss')
|
||||||
|
tag3 = Tag.create!(tag_name: tn3, category: :general)
|
||||||
|
|
||||||
|
other_post = Post.create!(
|
||||||
|
title: 'other post',
|
||||||
|
url: 'https://example.com/history-other'
|
||||||
|
)
|
||||||
|
|
||||||
|
# hit: add
|
||||||
|
PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
||||||
|
|
||||||
|
# hit: add + remove
|
||||||
|
pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
|
||||||
|
pt2.discard_by!(member)
|
||||||
|
|
||||||
|
# miss: add + remove
|
||||||
|
pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
|
||||||
|
pt3.discard_by!(member)
|
||||||
|
|
||||||
|
get '/posts/changes', params: { tag: tag2.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('changes', 'count')
|
||||||
|
expect(json['count']).to eq(3)
|
||||||
|
|
||||||
|
changes = json.fetch('changes')
|
||||||
|
|
||||||
|
expect(changes.map { |e| e.dig('tag', 'id') }.uniq).to eq([tag2.id])
|
||||||
|
expect(changes.map { |e| e['change_type'] }).to match_array(%w[add add remove])
|
||||||
|
expect(changes.map { |e| e.dig('post', 'id') }).to match_array([
|
||||||
|
post_record.id,
|
||||||
|
other_post.id,
|
||||||
|
other_post.id
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters history by post and tag together' do
|
||||||
|
tn2 = TagName.create!(name: 'history_tag_combo_hit')
|
||||||
|
tag2 = Tag.create!(tag_name: tn2, category: :general)
|
||||||
|
|
||||||
|
tn3 = TagName.create!(name: 'history_tag_combo_miss')
|
||||||
|
tag3 = Tag.create!(tag_name: tn3, category: :general)
|
||||||
|
|
||||||
|
other_post = Post.create!(
|
||||||
|
title: 'other combo post',
|
||||||
|
url: 'https://example.com/history-combo-other'
|
||||||
|
)
|
||||||
|
|
||||||
|
# hit
|
||||||
|
PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
||||||
|
|
||||||
|
# miss by post
|
||||||
|
pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
|
||||||
|
pt2.discard_by!(member)
|
||||||
|
|
||||||
|
# miss by tag
|
||||||
|
pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
|
||||||
|
pt3.discard_by!(member)
|
||||||
|
|
||||||
|
get '/posts/changes', params: { id: post_record.id, tag: tag2.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('changes', 'count')
|
||||||
|
expect(json['count']).to eq(1)
|
||||||
|
|
||||||
|
changes = json.fetch('changes')
|
||||||
|
expect(changes.size).to eq(1)
|
||||||
|
expect(changes[0]['change_type']).to eq('add')
|
||||||
|
expect(changes[0].dig('post', 'id')).to eq(post_record.id)
|
||||||
|
expect(changes[0].dig('tag', 'id')).to eq(tag2.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty history when tag does not match' do
|
||||||
|
tn2 = TagName.create!(name: 'history_tag_no_hit')
|
||||||
|
tag2 = Tag.create!(tag_name: tn2, category: :general)
|
||||||
|
|
||||||
|
get '/posts/changes', params: { tag: tag2.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json.fetch('changes')).to eq([])
|
||||||
|
expect(json.fetch('count')).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2049,7 +2024,6 @@ 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,
|
||||||
@@ -2071,7 +2045,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
let!(:v2) do
|
let!(:v2) do
|
||||||
post_record.post_tags.find_by!(tag: tag).destroy!
|
post_record.post_tags.kept.find_by!(tag: tag).discard_by!(member)
|
||||||
PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
||||||
post_record.update!(
|
post_record.update!(
|
||||||
title: 'updated spec post',
|
title: 'updated spec post',
|
||||||
|
|||||||
@@ -268,10 +268,6 @@ RSpec.describe Youtube::Sync do
|
|||||||
expect(tag_ids).to include(deerjikist_tag.id)
|
expect(tag_ids).to include(deerjikist_tag.id)
|
||||||
expect(tag_ids).not_to include(Tag.no_deerjikist.id)
|
expect(tag_ids).not_to include(Tag.no_deerjikist.id)
|
||||||
|
|
||||||
expect(PostTag.exists?(post:, tag: Tag.no_deerjikist)).to be(false)
|
|
||||||
expect(Tag.no_deerjikist.reload.post_count).to eq(0)
|
|
||||||
expect(deerjikist_tag.reload.post_count).to eq(1)
|
|
||||||
|
|
||||||
expect(PostVersionRecorder).to have_received(:ensure_snapshot!).with(
|
expect(PostVersionRecorder).to have_received(:ensure_snapshot!).with(
|
||||||
post,
|
post,
|
||||||
created_by_user: nil
|
created_by_user: nil
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ RSpec.describe 'nico:sync' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_tag!(name, category:)
|
def create_tag!(name, category:)
|
||||||
Tag.find_or_create_by_tag_name!(name, category:)
|
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
||||||
|
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) { |t| t.category = category }
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_nico_to_tag!(nico_tag, tag)
|
def link_nico_to_tag!(nico_tag, tag)
|
||||||
@@ -107,7 +108,7 @@ RSpec.describe 'nico:sync' do
|
|||||||
expect(calls).to eq(2)
|
expect(calls).to eq(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
it '古い nico tag の関連を物理削除し、変更前後の履歴を version に残す' do
|
it '既存 post にあった古い nico tag は active から外され、履歴として discard される' do
|
||||||
post = Post.create!(
|
post = Post.create!(
|
||||||
title: 'old',
|
title: 'old',
|
||||||
url: 'https://www.nicovideo.jp/watch/sm9',
|
url: 'https://www.nicovideo.jp/watch/sm9',
|
||||||
@@ -116,8 +117,8 @@ RSpec.describe 'nico:sync' do
|
|||||||
|
|
||||||
# 旧nicoタグ(今回の同期結果に含まれない)
|
# 旧nicoタグ(今回の同期結果に含まれない)
|
||||||
old_nico = create_tag!('nico:OLD', category: 'nico')
|
old_nico = create_tag!('nico:OLD', category: 'nico')
|
||||||
PostTag.create!(post:, tag: old_nico)
|
old_pt = PostTag.create!(post: post, tag: old_nico)
|
||||||
create_post_version_for!(post)
|
expect(old_pt.discarded_at).to be_nil
|
||||||
|
|
||||||
# 今回は NEW のみ欲しい
|
# 今回は NEW のみ欲しい
|
||||||
new_nico = create_tag!('nico:NEW', category: 'nico')
|
new_nico = create_tag!('nico:NEW', category: 'nico')
|
||||||
@@ -131,17 +132,9 @@ RSpec.describe 'nico:sync' do
|
|||||||
|
|
||||||
run_rake_task('nico:sync')
|
run_rake_task('nico:sync')
|
||||||
|
|
||||||
expect(PostTag.exists?(post:, tag: old_nico)).to be(false)
|
# OLD は active から外れる(discarded_at が入る)
|
||||||
expect(old_nico.reload.post_count).to eq(0)
|
old_pts = PostTag.where(post_id: post.id, tag_id: old_nico.id).order(:id).to_a
|
||||||
expect(new_nico.reload.post_count).to eq(1)
|
expect(old_pts.last.discarded_at).to be_present
|
||||||
|
|
||||||
versions = post.post_versions.order(:version_no)
|
|
||||||
expect(versions.first.tags_json.map { |item| item.fetch('id') })
|
|
||||||
.to include(old_nico.id)
|
|
||||||
expect(versions.last.tags_json.map { |item| item.fetch('id') })
|
|
||||||
.to include(new_nico.id)
|
|
||||||
expect(versions.last.tags_json.map { |item| item.fetch('id') })
|
|
||||||
.not_to include(old_nico.id)
|
|
||||||
|
|
||||||
# NEW は active にいる
|
# NEW は active にいる
|
||||||
post.reload
|
post.reload
|
||||||
@@ -163,7 +156,6 @@ 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,
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする