コミットを比較
103
コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
46e6d94edb | ||
|
|
817cd6ae73 | ||
|
|
dc7353304f | ||
|
|
5ff3fc9441 | ||
|
|
a847d93d2f | ||
|
|
2f87669699 | ||
|
|
1c906d7432 | ||
|
|
f662dc9dc0 | ||
|
|
206c6bc0a0 | ||
|
|
8f66ee8059 | ||
|
|
83e3db3314 | ||
|
|
eae6c30064 | ||
|
|
240e078f0b | ||
|
|
74b1ada0dd | ||
|
|
23d8adf65d | ||
|
|
e0debed94e | ||
|
|
583ce22a7e | ||
|
|
7c808a6f76 | ||
|
|
a5ae7c6f2d | ||
|
|
03dc4d0661 | ||
|
|
06b9c1cb50 | ||
|
|
688b4af575 | ||
|
|
040cc3f25d | ||
|
|
b3e67d8cca | ||
|
|
b00a62a0ac | ||
|
|
e6c3c635b8 | ||
|
|
9552081133 | ||
|
|
2d2a9b4bd6 | ||
|
|
09ac2576bb | ||
|
|
9eca670934 | ||
|
|
c31d84115d | ||
|
|
ff970f8171 | ||
|
|
0ae41b6266 | ||
|
|
eb4bf5e35c | ||
|
|
e6b7e33b83 | ||
|
|
3820d3d4d5 | ||
|
|
dd2d199d04 | ||
|
|
0a8ffc38b8 | ||
|
|
3f75994bd4 | ||
|
|
f91b78bd47 | ||
|
|
7f8cce39bc | ||
|
|
5c5a9fa1b0 | ||
|
|
66c738cbff | ||
|
|
cde0a2deae | ||
|
|
43a3772976 | ||
|
|
90d8d3ff08 | ||
|
|
d1de631eed | ||
|
|
6f3ccb0ffa | ||
|
|
95c3f08a44 | ||
|
|
ef95b20a7e | ||
|
|
f76fbe6711 | ||
|
|
0224c4d2f4 | ||
|
|
ead825e3ee | ||
|
|
3d88fdecb1 | ||
|
|
2e1b4449ba | ||
|
|
4eb483c049 | ||
|
|
e197f27e9a | ||
|
|
34f81325d2 | ||
|
|
9af4670c6c | ||
|
|
5183b0b354 | ||
|
|
d0914fce3b | ||
|
|
0ac7332458 | ||
|
|
07ce19e32d | ||
|
|
5f1d619139 | ||
|
|
3cd22855b3 | ||
|
|
55c69866a7 | ||
|
|
04d2f0f5a9 | ||
|
|
025f49cbcb | ||
|
|
d2f1b1ed48 | ||
|
|
6e5aa1e30f | ||
|
|
21d6c1408a | ||
|
|
eebbd5d069 | ||
|
|
58828597d7 | ||
|
|
b0c24f319a | ||
|
|
f9463f383f | ||
|
|
6d037192c4 | ||
|
|
d035da99ad | ||
|
|
bc660676ef | ||
|
|
97b132e5ab | ||
|
|
be68841bd3 | ||
|
|
ac82adc6b3 | ||
|
|
aa96ec95d1 | ||
|
|
df17f20907 | ||
|
|
769966648b | ||
|
|
7bcb76516c | ||
|
|
8970edc59f | ||
|
|
9b1ea56e36 | ||
|
|
155edfe018 | ||
|
|
23f1ffb04b | ||
|
|
ccfe65a6a4 | ||
|
|
06d6e512e4 | ||
|
|
5bd097bcfe | ||
|
|
f636d2a177 | ||
|
|
10dc776313 | ||
|
|
a9e16735f8 | ||
|
|
440a6c9961 | ||
|
|
4535a9d260 | ||
|
|
440d3d38be | ||
|
|
cb33d9ca25 | ||
|
|
19bf24432a | ||
|
|
ac41385962 | ||
|
|
08bf92ff79 | ||
|
|
c51d7b98ba |
@@ -85,13 +85,4 @@ class ApplicationController < ActionController::API
|
||||
base_errors: },
|
||||
status:
|
||||
end
|
||||
|
||||
def normalise_json value
|
||||
return nil if value.nil?
|
||||
return JSON.parse(value) if value.is_a?(String)
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese
|
||||
end
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
class DeerjikistsController < ApplicationController
|
||||
def show
|
||||
locale = resolve_locale!
|
||||
|
||||
platform = params[:platform].to_s.strip
|
||||
code = params[:code].to_s.strip
|
||||
return render_bad_request('platform は必須です.') if platform.blank?
|
||||
@@ -9,10 +7,10 @@ class DeerjikistsController < ApplicationController
|
||||
|
||||
deerjikist = Deerjikist
|
||||
.joins(:tag)
|
||||
.includes(tag: :tag_names)
|
||||
.includes(tag: :tag_name)
|
||||
.find_by(platform:, code:)
|
||||
if deerjikist
|
||||
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
|
||||
render json: DeerjikistRepr.base(deerjikist)
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
@@ -22,7 +20,6 @@ class DeerjikistsController < ApplicationController
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
locale = resolve_locale!
|
||||
platform = params[:platform].to_s.strip
|
||||
code = params[:code].to_s.strip
|
||||
tag_id = params[:tag_id].to_i
|
||||
@@ -35,7 +32,7 @@ class DeerjikistsController < ApplicationController
|
||||
d.save!
|
||||
end
|
||||
|
||||
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
|
||||
render json: DeerjikistRepr.base(deerjikist)
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -2,7 +2,7 @@ class GekanatorPostsController < ApplicationController
|
||||
def index
|
||||
posts =
|
||||
Post
|
||||
.preload(:post_similarities, tags: :tag_names)
|
||||
.preload(:post_similarities, tags: :tag_name)
|
||||
.with_attached_thumbnail
|
||||
.order(Arel.sql(
|
||||
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
|
||||
@@ -56,7 +56,7 @@ class GekanatorPostsController < ApplicationController
|
||||
def tag_json tag
|
||||
{
|
||||
id: tag.id,
|
||||
name: tag.name('ja'),
|
||||
name: tag.name,
|
||||
category: tag.category
|
||||
}
|
||||
end
|
||||
|
||||
@@ -126,9 +126,9 @@ class GekanatorQuestionsController < ApplicationController
|
||||
end
|
||||
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.joins(:tag_name)
|
||||
.where(category: categories.uniq)
|
||||
.where(tag_names: { language_code: 'ja', primary_flg: true, name: names.uniq })
|
||||
.where(tag_names: { name: names.uniq })
|
||||
.where.not(deprecated_at: nil)
|
||||
.pluck('tags.category', 'tag_names.name')
|
||||
.each_with_object({ }) do |(category, name), h|
|
||||
|
||||
@@ -7,8 +7,6 @@ class MaterialsController < ApplicationController
|
||||
rescue_from MaterialZipExporter::MissingFileError, with: :render_zip_missing_file
|
||||
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
|
||||
page = (params[:page].presence || 1).to_i
|
||||
limit = (params[:limit].presence || 20).to_i
|
||||
|
||||
@@ -22,7 +20,7 @@ class MaterialsController < ApplicationController
|
||||
q = Material.includes(:material_export_items,
|
||||
thumbnail_attachment: :blob,
|
||||
file_attachment: :blob,
|
||||
tag: :tag_names)
|
||||
tag: :tag_name)
|
||||
q = q.where(tag_id: nil) if filters[:tag_state] == 'untagged'
|
||||
q = q.where.not(tag_id: nil) if filters[:tag_state] == 'tagged'
|
||||
q = apply_material_tag_filter(q, filters, tag_graph)
|
||||
@@ -44,10 +42,7 @@ class MaterialsController < ApplicationController
|
||||
.offset(offset)
|
||||
.to_a
|
||||
|
||||
response = { materials: MaterialRepr.list_many(
|
||||
materials,
|
||||
host: request.base_url,
|
||||
language_code: locale.language_code),
|
||||
response = { materials: MaterialRepr.list_many(materials, host: request.base_url),
|
||||
count: }
|
||||
if filters[:tag_id]
|
||||
response[:tag_scope] = material_index_tag_scope(filters, tag_graph)
|
||||
@@ -60,8 +55,6 @@ class MaterialsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
locale = resolve_locale!
|
||||
|
||||
material =
|
||||
Material
|
||||
.includes(:tag, :material_export_items)
|
||||
@@ -70,17 +63,15 @@ class MaterialsController < ApplicationController
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless material
|
||||
|
||||
wiki_page_body = material.tag&.tag_name('ja')&.wiki_page&.current_revision&.body
|
||||
wiki_page_body = material.tag&.tag_name&.wiki_page&.current_revision&.body
|
||||
|
||||
render json: MaterialRepr.base(material, host: request.base_url,
|
||||
language_code: locale.language_code).merge(wiki_page_body:)
|
||||
render json: MaterialRepr.base(material, host: request.base_url).merge(wiki_page_body:)
|
||||
end
|
||||
|
||||
def create
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
locale = resolve_locale!
|
||||
tag_name_raw = params[:tag].to_s.strip
|
||||
file = params[:file]
|
||||
file_sha256 = MaterialFileSha256.from_upload(file)
|
||||
@@ -98,14 +89,13 @@ class MaterialsController < ApplicationController
|
||||
|
||||
begin
|
||||
Material.transaction do
|
||||
tag = resolve_material_tag!(locale, tag_name_raw)
|
||||
tag = resolve_material_tag!(tag_name_raw)
|
||||
material = Material.new(tag:, url:,
|
||||
created_by_user: current_user,
|
||||
updated_by_user: current_user)
|
||||
material.file.attach(uploaded_blob) if uploaded_blob
|
||||
material.save!
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
|
||||
upsert_export_paths!(material)
|
||||
MaterialVersionRecorder.record!(material:, event_type: :create,
|
||||
created_by_user: current_user)
|
||||
@@ -117,8 +107,7 @@ class MaterialsController < ApplicationController
|
||||
|
||||
if material
|
||||
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
||||
render json: MaterialRepr.base(material, host: request.base_url,
|
||||
language_code: locale.language_code), status: :created
|
||||
render json: MaterialRepr.base(material, host: request.base_url), status: :created
|
||||
else
|
||||
render_validation_error material
|
||||
end
|
||||
@@ -143,14 +132,12 @@ class MaterialsController < ApplicationController
|
||||
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
||||
return render_material_import_block(block) if block
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
uploaded_blob = build_uploaded_material_blob!(file, file_sha256)
|
||||
|
||||
begin
|
||||
Material.transaction do
|
||||
MaterialVersionRecorder.ensure_snapshot!(material, created_by_user: current_user)
|
||||
tag = resolve_material_tag!(locale, tag_name_raw)
|
||||
tag = resolve_material_tag!(tag_name_raw)
|
||||
material.assign_attributes(tag:, url:, updated_by_user: current_user)
|
||||
if uploaded_blob
|
||||
material.file.attach(uploaded_blob)
|
||||
@@ -158,8 +145,7 @@ class MaterialsController < ApplicationController
|
||||
material.file.detach
|
||||
end
|
||||
material.save!
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
|
||||
upsert_export_paths!(material)
|
||||
MaterialVersionRecorder.record!(material:, event_type: :update,
|
||||
created_by_user: current_user)
|
||||
@@ -171,8 +157,7 @@ class MaterialsController < ApplicationController
|
||||
|
||||
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
||||
|
||||
render json: MaterialRepr.base(material, host: request.base_url,
|
||||
language_code: locale.language_code)
|
||||
render json: MaterialRepr.base(material, host: request.base_url)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -251,8 +236,10 @@ class MaterialsController < ApplicationController
|
||||
nil
|
||||
end
|
||||
|
||||
def resolve_material_tag! locale, tag_name_raw
|
||||
Tag.find_or_create_by_tag_name!(locale, tag_name_raw, category: :material)
|
||||
def resolve_material_tag! tag_name_raw
|
||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||
tag = tag_name.tag
|
||||
tag || Tag.create!(tag_name:, category: :material)
|
||||
end
|
||||
|
||||
def material_index_needs_tag_name? filters
|
||||
@@ -266,12 +253,7 @@ class MaterialsController < ApplicationController
|
||||
end
|
||||
|
||||
def material_index_join_tag_name q
|
||||
q.left_joins(:tag).joins(<<~SQL.squish)
|
||||
LEFT JOIN tag_names
|
||||
ON tag_names.tag_id = tags.id
|
||||
AND tag_names.language_code = 'ja'
|
||||
AND tag_names.primary_flg = TRUE
|
||||
SQL
|
||||
q.left_joins(tag: :tag_name)
|
||||
end
|
||||
|
||||
def material_index_join_file_blob q
|
||||
@@ -287,11 +269,9 @@ class MaterialsController < ApplicationController
|
||||
|
||||
def apply_material_query q, term
|
||||
like = "%#{ ActiveRecord::Base.sanitize_sql_like(term) }%"
|
||||
q.where(<<~SQL.squish, q: like)
|
||||
tag_names.name LIKE :q
|
||||
OR materials.url LIKE :q
|
||||
OR material_file_blobs.filename LIKE :q
|
||||
SQL
|
||||
q.where('tag_names.name LIKE :q OR materials.url LIKE :q OR ' \
|
||||
'material_file_blobs.filename LIKE :q',
|
||||
q: like)
|
||||
end
|
||||
|
||||
def apply_material_media_kind q, media_kind
|
||||
@@ -343,9 +323,8 @@ class MaterialsController < ApplicationController
|
||||
end
|
||||
tags_by_id =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(id: scope_tag_ids,
|
||||
tag_names: { language_code: 'ja', primary_flg: true })
|
||||
.joins(:tag_name)
|
||||
.where(id: scope_tag_ids)
|
||||
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
||||
.each_with_object({ }) do |(id, name, category, deprecated_at), hash|
|
||||
hash[id] = { id:, name:, category:, deprecated: deprecated_at.present? }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
class NicoTagsController < ApplicationController
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
|
||||
name = params[:name].presence
|
||||
linked_tag = params[:linked_tag].presence
|
||||
link_status = params[:link_status].presence
|
||||
@@ -17,27 +15,21 @@ class NicoTagsController < ApplicationController
|
||||
limit = 1 if limit < 1
|
||||
|
||||
post_tag_max_sql =
|
||||
PostExternalTag
|
||||
.select('external_tag_id, MAX(created_at) AS max_created_at')
|
||||
.group('external_tag_id')
|
||||
PostTag
|
||||
.select('tag_id, MAX(created_at) AS max_created_at')
|
||||
.group('tag_id')
|
||||
.to_sql
|
||||
|
||||
q =
|
||||
ExternalTag
|
||||
q = Tag.nico_tags
|
||||
.joins(:tag_name)
|
||||
.joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \
|
||||
'ON post_tag_max.external_tag_id = external_tags.id')
|
||||
.includes(linked_tags: { tag_names: :wiki_page })
|
||||
if name
|
||||
q = q.where(('external_tags.name LIKE ? ' +
|
||||
"OR CONCAT(external_tags.platform, ':', external_tags.name) LIKE ?"),
|
||||
"%#{ name }%", "%#{ name }")
|
||||
end
|
||||
|
||||
'ON post_tag_max.tag_id = tags.id')
|
||||
.includes(:tag_name, tag_name: :wiki_page, linked_tags: { tag_name: :wiki_page })
|
||||
q = q.where('tag_names.name LIKE ?', "%#{ name }%") if name
|
||||
if linked_tag
|
||||
linked_tag_ids =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: 'ja', primary_flg: true })
|
||||
.joins(:tag_name)
|
||||
.where('tag_names.name LIKE ?', "%#{ linked_tag }%")
|
||||
.pluck(:id)
|
||||
linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id)
|
||||
@@ -46,7 +38,7 @@ class NicoTagsController < ApplicationController
|
||||
if link_status.in?(['linked', 'unlinked'])
|
||||
exists_sql =
|
||||
'EXISTS (SELECT 1 FROM nico_tag_relations ' \
|
||||
'WHERE nico_tag_relations.nico_tag_id = external_tags.id)'
|
||||
'WHERE nico_tag_relations.nico_tag_id = tags.id)'
|
||||
q = link_status == 'linked' ? q.where(exists_sql) : q.where("NOT #{ exists_sql }")
|
||||
end
|
||||
|
||||
@@ -54,68 +46,54 @@ class NicoTagsController < ApplicationController
|
||||
sort_sql =
|
||||
case order[0]
|
||||
when 'name'
|
||||
'external_tags.name'
|
||||
'tag_names.name'
|
||||
when 'updated_at'
|
||||
'post_tag_max.max_created_at'
|
||||
else
|
||||
"external_tags.#{ order[0] }"
|
||||
"tags.#{ order[0] }"
|
||||
end
|
||||
tags = q.reselect('external_tags.*',
|
||||
tags = q.reselect('tags.*',
|
||||
Arel.sql('post_tag_max.max_created_at AS recent_post_tag_created_at'))
|
||||
.order(Arel.sql("#{ sort_sql } #{ order[1] }, external_tags.id #{ order[1] }"))
|
||||
.order(Arel.sql("#{ sort_sql } #{ order[1] }, tags.id #{ order[1] }"))
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit)
|
||||
.to_a
|
||||
|
||||
render json: { tags: tags.map { |tag|
|
||||
external_tag_json(tag).merge(
|
||||
TagRepr.base(tag).merge(
|
||||
recent_post_tag_created_at: tag.recent_post_tag_created_at,
|
||||
linked_tags: tag.linked_tags.map { |lt|
|
||||
TagRepr.base(lt, language_code: locale.language_code)
|
||||
})
|
||||
linked_tags: tag.linked_tags.map { |lt| TagRepr.base(lt) })
|
||||
}, count: }
|
||||
end
|
||||
|
||||
def show
|
||||
tag = ExternalTag.find(params[:id])
|
||||
|
||||
render json: external_tag_json(tag)
|
||||
end
|
||||
|
||||
def update
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
id = params[:id].to_i
|
||||
|
||||
tag = ExternalTag.find(id)
|
||||
tag = Tag.find(id)
|
||||
return render_bad_request('ニコニコ・タグを指定してください.') unless tag.nico?
|
||||
|
||||
linked_tag_names = params[:tags].to_s.split
|
||||
linked_tags = nil
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
linked_tags = Tag.normalise_tags!(locale, linked_tag_names,
|
||||
with_tagme: false,
|
||||
linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
if linked_tags.any? { |t| t.nico? }
|
||||
raise Tag::NicoTagNormalisationError
|
||||
end
|
||||
|
||||
TagVersioning.record_tag_snapshots!(
|
||||
linked_tags,
|
||||
created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
TagVersioning.record_tag_snapshots!(linked_tags, created_by_user: current_user)
|
||||
|
||||
tag.linked_tags = linked_tags
|
||||
tag.save!
|
||||
|
||||
NicoTagVersionRecorder.record!(external_tag: tag,
|
||||
event_type: :update,
|
||||
created_by_user: current_user)
|
||||
NicoTagVersionRecorder.record!(tag:, event_type: :update, created_by_user: current_user)
|
||||
end
|
||||
|
||||
render json: tag.linked_tags.map { |t|
|
||||
TagRepr.base(t, language_code: locale.language_code)
|
||||
}, status: :ok
|
||||
render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
render_validation_error fields: { tags: ['ニコニコ・タグ同士は連携できません.'] }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@@ -124,21 +102,6 @@ class NicoTagsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def external_tag_json tag
|
||||
{ id: tag.id,
|
||||
name: "#{ tag.platform }:#{ tag.name }",
|
||||
category: 'nico',
|
||||
post_count: tag.post_count,
|
||||
created_at: tag.created_at,
|
||||
updated_at: tag.created_at,
|
||||
deprecated_at: nil,
|
||||
aliases: [],
|
||||
parents: [],
|
||||
has_wiki: false,
|
||||
material_id: nil,
|
||||
has_deerjikists: false }
|
||||
end
|
||||
|
||||
def render_nico_tag_form_record_invalid record
|
||||
if record.is_a?(TagName) || record.is_a?(Tag)
|
||||
render_validation_error fields: { tags: record.errors.full_messages.map { |message|
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
class PostVersionsController < ApplicationController
|
||||
def index
|
||||
post_id = params[:post].presence
|
||||
tag_id = params[:tag].presence&.to_i
|
||||
external_tag_id = params[:external_tag].presence&.to_i
|
||||
return head :bad_request if tag_id && external_tag_id
|
||||
|
||||
tag_id = params[:tag].presence
|
||||
page = (params[:page].presence || 1).to_i
|
||||
limit = (params[:limit].presence || 20).to_i
|
||||
|
||||
@@ -13,6 +10,12 @@ class PostVersionsController < ApplicationController
|
||||
|
||||
offset = (page - 1) * limit
|
||||
|
||||
tag_name =
|
||||
if tag_id
|
||||
TagName.joins(:tag).find_by(tag: { id: tag_id })
|
||||
end
|
||||
return render json: { versions: [], count: 0 } if tag_id && tag_name.blank?
|
||||
|
||||
q = PostVersion.joins(<<~SQL.squish)
|
||||
LEFT JOIN
|
||||
post_versions prev
|
||||
@@ -20,23 +23,17 @@ class PostVersionsController < ApplicationController
|
||||
prev.post_id = post_versions.post_id
|
||||
AND prev.version_no = post_versions.version_no - 1
|
||||
SQL
|
||||
.select('post_versions.*',
|
||||
'prev.title AS prev_title',
|
||||
'prev.url AS prev_url',
|
||||
'prev.thumbnail_base AS prev_thumbnail_base',
|
||||
'prev.tags_json AS prev_tags_json',
|
||||
.select('post_versions.*', 'prev.title AS prev_title', 'prev.url AS prev_url',
|
||||
'prev.thumbnail_base AS prev_thumbnail_base', 'prev.tags AS prev_tags',
|
||||
'prev.video_ms AS prev_video_ms',
|
||||
'prev.original_created_from AS prev_original_created_from',
|
||||
'prev.original_created_before AS prev_original_created_before')
|
||||
q = q.where('post_versions.post_id = ?', post_id) if post_id
|
||||
if external_tag_id || (tag_id && !(Tag.exists?(id: tag_id)))
|
||||
q = q.where('JSON_CONTAINS(post_versions.tags_json,' +
|
||||
"JSON_OBJECT('external_tag_id', #{ external_tag_id || tag_id })) " +
|
||||
'OR JSON_CONTAINS(prev.tags_json,' +
|
||||
"JSON_OBJECT('external_tag_id', #{ external_tag_id || tag_id }))")
|
||||
elsif tag_id
|
||||
q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('tag_id', #{ tag_id })) " +
|
||||
"OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('tag_id', #{ tag_id }))")
|
||||
if tag_name
|
||||
escaped = ActiveRecord::Base.sanitize_sql_like(tag_name.name)
|
||||
q = q.where(("CONCAT(' ', post_versions.tags, ' ') LIKE :kw " +
|
||||
"OR CONCAT(' ', prev.tags, ' ') LIKE :kw"),
|
||||
kw: "% #{ escaped } %")
|
||||
end
|
||||
|
||||
count = q.except(:select, :order, :limit, :offset).count
|
||||
@@ -51,80 +48,59 @@ class PostVersionsController < ApplicationController
|
||||
private
|
||||
|
||||
def serialise_versions rows
|
||||
rows = rows.to_a
|
||||
user_ids = rows.map(&:created_by_user_id).compact.uniq
|
||||
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
|
||||
snapshots = rows.flat_map { |row|
|
||||
[normalise_json(row.tags_json),
|
||||
normalise_json(row.attributes['prev_tags_json']) || []]
|
||||
}
|
||||
external_tag_names = external_tag_names_for(snapshots)
|
||||
|
||||
rows.map do |row|
|
||||
cur_tags = snapshot_tag_literals(normalise_json(row.tags_json), external_tag_names)
|
||||
prev_tags = snapshot_tag_literals(
|
||||
normalise_json(row.attributes['prev_tags_json']) || [], external_tag_names)
|
||||
cur_tags = split_tags(row.tags)
|
||||
prev_tags = split_tags(row.attributes['prev_tags'])
|
||||
|
||||
{ post_id: row.post_id,
|
||||
{
|
||||
post_id: row.post_id,
|
||||
version_no: row.version_no,
|
||||
event_type: row.event_type,
|
||||
title: { current: row.title, prev: row.attributes['prev_title'] },
|
||||
url: { current: row.url, prev: row.attributes['prev_url'] },
|
||||
thumbnail: { current: nil, prev: nil },
|
||||
thumbnail_base: { current: row.thumbnail_base,
|
||||
prev: row.attributes['prev_thumbnail_base'] },
|
||||
video_ms: { current: row.video_ms, prev: row.attributes['prev_video_ms'] },
|
||||
title: {
|
||||
current: row.title,
|
||||
prev: row.attributes['prev_title']
|
||||
},
|
||||
url: {
|
||||
current: row.url,
|
||||
prev: row.attributes['prev_url']
|
||||
},
|
||||
thumbnail: {
|
||||
current: nil,
|
||||
prev: nil
|
||||
},
|
||||
thumbnail_base: {
|
||||
current: row.thumbnail_base,
|
||||
prev: row.attributes['prev_thumbnail_base']
|
||||
},
|
||||
video_ms: {
|
||||
current: row.video_ms,
|
||||
prev: row.attributes['prev_video_ms']
|
||||
},
|
||||
tags: build_version_tags(cur_tags, prev_tags),
|
||||
original_created_from: {
|
||||
current: row.original_created_from&.iso8601,
|
||||
prev: row.attributes['prev_original_created_from']&.iso8601 },
|
||||
prev: row.attributes['prev_original_created_from']&.iso8601
|
||||
},
|
||||
original_created_before: {
|
||||
current: row.original_created_before&.iso8601,
|
||||
prev: row.attributes['prev_original_created_before']&.iso8601 },
|
||||
prev: row.attributes['prev_original_created_before']&.iso8601
|
||||
},
|
||||
created_at: row.created_at.iso8601,
|
||||
created_by_user:
|
||||
if row.created_by_user_id
|
||||
{ id: row.created_by_user_id,
|
||||
name: users_by_id[row.created_by_user_id] }
|
||||
end }
|
||||
end
|
||||
end
|
||||
|
||||
def external_tag_names_for snapshots
|
||||
ids = snapshots.flatten.filter_map { _1['external_tag_id'] }.uniq
|
||||
names = ExternalTag.where(id: ids).pluck(:id, :platform, :name).to_h { |id, platform, name|
|
||||
[id, "#{ platform }:#{ name }"]
|
||||
{
|
||||
id: row.created_by_user_id,
|
||||
name: users_by_id[row.created_by_user_id]
|
||||
}
|
||||
missing_ids = ids - names.keys
|
||||
|
||||
NicoTagVersion
|
||||
.where(tag_id: missing_ids)
|
||||
.order(:tag_id, version_no: :desc)
|
||||
.pluck(:tag_id, :name)
|
||||
.each { |id, name| names[id] ||= name }
|
||||
|
||||
names
|
||||
end
|
||||
|
||||
def snapshot_tag_literals snapshots, external_tag_names
|
||||
snapshots
|
||||
.filter_map { |snapshot|
|
||||
if snapshot.key?('tag_id')
|
||||
[tag_category_order(snapshot['category']),
|
||||
Post.tag_snapshot_literal(snapshot)]
|
||||
elsif external_tag_names[snapshot['external_tag_id']]
|
||||
[6, external_tag_names[snapshot['external_tag_id']]]
|
||||
end
|
||||
}
|
||||
.sort_by { |order, name| [order, name.downcase] }
|
||||
.map(&:second)
|
||||
end
|
||||
end
|
||||
|
||||
def tag_category_order category
|
||||
['deerjikist', 'meme', 'character', 'general', 'material', 'meta'].index(category) || 6
|
||||
end
|
||||
|
||||
def build_version_tags cur_tags, prev_tags
|
||||
def build_version_tags(cur_tags, prev_tags)
|
||||
(cur_tags | prev_tags).map do |name|
|
||||
type =
|
||||
if cur_tags.include?(name) && prev_tags.include?(name)
|
||||
@@ -135,7 +111,14 @@ class PostVersionsController < ApplicationController
|
||||
'removed'
|
||||
end
|
||||
|
||||
{ name:, type: }
|
||||
{
|
||||
name:,
|
||||
type:
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def split_tags(tags)
|
||||
tags.to_s.split(/\s+/).reject(&:blank?)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,8 +7,6 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
|
||||
url = params[:url].presence
|
||||
title = params[:title].presence
|
||||
original_created_from = params[:original_created_from].presence
|
||||
@@ -38,8 +36,8 @@ class PostsController < ApplicationController
|
||||
offset = (page - 1) * limit
|
||||
|
||||
pt_max_sql =
|
||||
PostVersion
|
||||
.select('post_id, MAX(created_at) AS max_updated_at')
|
||||
PostTag
|
||||
.select('post_id, MAX(updated_at) AS max_updated_at')
|
||||
.group('post_id')
|
||||
.to_sql
|
||||
|
||||
@@ -48,12 +46,13 @@ class PostsController < ApplicationController
|
||||
'COALESCE(pt_max.max_updated_at, posts.updated_at))'
|
||||
|
||||
q =
|
||||
filtered_posts(locale)
|
||||
filtered_posts
|
||||
.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"))
|
||||
.preload(:external_tags, :uploaded_user, :parents, :children,
|
||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||
{ tag_names: :wiki_page }] }])
|
||||
.preload(:uploaded_user, :parents, :children,
|
||||
active_post_tags: [:sections,
|
||||
{ tag: [:deerjikists, :materials,
|
||||
{ tag_name: :wiki_page }] }])
|
||||
.with_attached_thumbnail
|
||||
|
||||
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
||||
@@ -92,8 +91,7 @@ class PostsController < ApplicationController
|
||||
q = q.except(:select, :order)
|
||||
|
||||
render json: { posts: posts.map { |post|
|
||||
PostRepr.base(post, language_code: locale.language_code)
|
||||
.merge(updated_at: post.updated_at_all).tap do |json|
|
||||
PostRepr.base(post).merge(updated_at: post.updated_at_all).tap do |json|
|
||||
json['thumbnail'] =
|
||||
if post.thumbnail.attached?
|
||||
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
||||
@@ -105,19 +103,16 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def random
|
||||
locale = resolve_locale!
|
||||
|
||||
post =
|
||||
filtered_posts(locale)
|
||||
.preload(:uploaded_user, :parents, :children,
|
||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||
{ tag_names: :wiki_page }] }])
|
||||
post = filtered_posts.preload(:uploaded_user, :parents, :children,
|
||||
active_post_tags: [:sections,
|
||||
{ tag: [:deerjikists, :materials,
|
||||
{ tag_name: :wiki_page }] }])
|
||||
.with_attached_thumbnail
|
||||
.order('RAND()')
|
||||
.first
|
||||
return head :not_found unless post
|
||||
|
||||
render json: PostRepr.base(post, current_user, language_code: locale.language_code)
|
||||
render json: PostRepr.base(post, current_user)
|
||||
end
|
||||
|
||||
def metadata
|
||||
@@ -192,13 +187,12 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
locale = resolve_locale!
|
||||
|
||||
post =
|
||||
Post
|
||||
.includes(:uploaded_user, :parents, :children,
|
||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||
{ tag_names: :wiki_page }] }])
|
||||
active_post_tags: [:sections,
|
||||
{ tag: [:deerjikists, :materials,
|
||||
{ tag_name: :wiki_page }] }])
|
||||
.with_attached_thumbnail
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless post
|
||||
@@ -212,21 +206,18 @@ class PostsController < ApplicationController
|
||||
parent_posts:,
|
||||
child_posts:,
|
||||
sibling_posts:,
|
||||
related:,
|
||||
language_code: locale.language_code)
|
||||
.merge(tags: build_tag_tree_for(post, locale.language_code))
|
||||
related:)
|
||||
.merge(tags: build_tag_tree_for(post))
|
||||
end
|
||||
|
||||
def create
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
preflight = PostCreatePreflight.new(
|
||||
attributes: post_create_attributes,
|
||||
thumbnail: params[:thumbnail],
|
||||
host: request.base_url).run(locale)
|
||||
host: request.base_url).run
|
||||
return render json: dry_run_json(preflight) if bool?(:dry)
|
||||
if preflight[:existing_post_id].present?
|
||||
post = Post.new(url: preflight[:url])
|
||||
@@ -252,10 +243,10 @@ class PostsController < ApplicationController
|
||||
:post_tag_specs,
|
||||
:tag_sections,
|
||||
:normalised_parent_post_ids).symbolize_keys).merge(
|
||||
thumbnail: params[:thumbnail])).create!(locale)
|
||||
thumbnail: params[:thumbnail])).create!
|
||||
|
||||
post.reload
|
||||
render json: PostRepr.base(post, language_code: locale.language_code), status: :created
|
||||
render json: PostRepr.base(post), status: :created
|
||||
rescue PostCreatePreflight::ValidationFailed => e
|
||||
render_validation_error fields: e.fields, base: e.base_errors
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
@@ -281,18 +272,13 @@ class PostsController < ApplicationController
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
return head :unsupported_media_type unless request.content_mime_type == Mime[:multipart_form]
|
||||
return head :payload_too_large if request.content_length.to_i > MAX_BULK_REQUEST_BYTES
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
posts = parse_bulk_posts_manifest
|
||||
thumbnails = parse_bulk_thumbnails(posts.length)
|
||||
|
||||
result = PostBulkCreator.new(
|
||||
actor: current_user,
|
||||
posts:,
|
||||
thumbnails:,
|
||||
host: request.base_url).run(locale)
|
||||
|
||||
host: request.base_url).run
|
||||
render json: result
|
||||
rescue JSON::ParserError
|
||||
render_bad_request 'posts manifest の JSON が不正です.'
|
||||
@@ -325,7 +311,6 @@ class PostsController < ApplicationController
|
||||
base_version_no = parse_base_version_no
|
||||
return render_bad_request('base_version_no は必須です.') if !(force) && !(base_version_no)
|
||||
|
||||
locale = Locale.find_by(code: params[:locale].presence) || Locale.nipponese
|
||||
title = params[:title].presence
|
||||
tag_names = params[:tags].to_s.split
|
||||
original_created_from = params[:original_created_from]
|
||||
@@ -345,10 +330,9 @@ class PostsController < ApplicationController
|
||||
base_version = post.post_versions.find_by!(version_no: base_version_no)
|
||||
|
||||
base_snapshot = post_snapshot_from_version(base_version)
|
||||
current_snapshot = post_snapshot_from_record(post, locale)
|
||||
current_snapshot = post_snapshot_from_record(post)
|
||||
end
|
||||
incoming_snapshot = post_incoming_snapshot(locale:,
|
||||
title:,
|
||||
incoming_snapshot = post_incoming_snapshot(title:,
|
||||
original_created_from:,
|
||||
original_created_before:,
|
||||
tag_names:,
|
||||
@@ -377,14 +361,14 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
apply_post_snapshot!(locale, post, snapshot_to_apply)
|
||||
apply_post_snapshot!(post, snapshot_to_apply)
|
||||
end
|
||||
|
||||
return render json: conflict_json, status: :conflict if conflict_json
|
||||
|
||||
post.reload
|
||||
json = PostRepr.base(post, current_user, language_code: locale.language_code)
|
||||
json['tags'] = build_tag_tree_for(post, locale.language_code)
|
||||
json = PostRepr.base(post, current_user)
|
||||
json['tags'] = build_tag_tree_for(post)
|
||||
render json:, status: :ok
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
|
||||
@@ -400,21 +384,65 @@ class PostsController < ApplicationController
|
||||
render_post_form_record_invalid e.record
|
||||
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
|
||||
|
||||
def filtered_posts locale
|
||||
def filtered_posts
|
||||
tag_names = params[:tags].to_s.split
|
||||
match_type = params[:match]
|
||||
if tag_names.present?
|
||||
filter_posts_by_tags(locale, tag_names, match_type)
|
||||
filter_posts_by_tags(tag_names, match_type)
|
||||
else
|
||||
Post.all
|
||||
end
|
||||
end
|
||||
|
||||
def filter_posts_by_tags locale, tag_names, match_type
|
||||
def filter_posts_by_tags tag_names, match_type
|
||||
literals = tag_names.map do |raw_name|
|
||||
{ name: TagName.canonicalise(locale, raw_name.sub(/\Anot:/i, '')).first,
|
||||
{ name: TagName.canonicalise(raw_name.sub(/\Anot:/i, '')).first,
|
||||
negative: raw_name.downcase.start_with?('not:') }
|
||||
end
|
||||
|
||||
@@ -422,11 +450,11 @@ class PostsController < ApplicationController
|
||||
|
||||
if match_type == 'any'
|
||||
literals.reduce(Post.none) do |posts, literal|
|
||||
posts.or(tag_literal_relation(locale, literal[:name], negative: literal[:negative]))
|
||||
posts.or(tag_literal_relation(literal[:name], negative: literal[:negative]))
|
||||
end
|
||||
else
|
||||
literals.reduce(Post.all) do |posts, literal|
|
||||
ids = tagged_post_ids_for(locale, literal[:name])
|
||||
ids = tagged_post_ids_for(literal[:name])
|
||||
if literal[:negative]
|
||||
posts.where.not(id: ids)
|
||||
else
|
||||
@@ -436,8 +464,8 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def tag_literal_relation locale, name, negative:
|
||||
ids = tagged_post_ids_for(locale, name)
|
||||
def tag_literal_relation name, negative:
|
||||
ids = tagged_post_ids_for(name)
|
||||
if negative
|
||||
Post.where.not(id: ids)
|
||||
else
|
||||
@@ -445,20 +473,8 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def tagged_post_ids_for locale, name
|
||||
posts_by_internal_tags =
|
||||
Post
|
||||
.joins(tags: :tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, name: })
|
||||
.select(:id)
|
||||
|
||||
posts_by_external_tags =
|
||||
Post
|
||||
.joins(:external_tags)
|
||||
.where("CONCAT(external_tags.platform, ':', external_tags.name) = ?", name)
|
||||
|
||||
(posts_by_internal_tags + posts_by_external_tags).uniq(&:id)
|
||||
end
|
||||
def tagged_post_ids_for(name) =
|
||||
Post.joins(tags: :tag_name).where(tag_names: { name: }).select(:id)
|
||||
|
||||
def sync_post_tags! post, desired_tags, sections
|
||||
desired_tags.each do |t|
|
||||
@@ -486,13 +502,13 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each do |pt|
|
||||
pt.destroy!
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||
pt.discard_by!(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def build_tag_tree_for post, language_code = 'ja'
|
||||
post_tags = post.post_tags.reject { |post_tag| post_tag.tag.deprecated? }
|
||||
def build_tag_tree_for post
|
||||
post_tags = post.active_post_tags.reject { |post_tag| post_tag.tag.deprecated? }
|
||||
tags = post_tags.map(&:tag)
|
||||
tag_ids = tags.map(&:id)
|
||||
|
||||
@@ -521,7 +537,7 @@ class PostsController < ApplicationController
|
||||
sections = sections_by_tag_id.fetch(tag_id, [])
|
||||
|
||||
if path.include?(tag_id)
|
||||
return TagRepr.inline(tag, language_code:).merge(children: [], sections:)
|
||||
return TagRepr.inline(tag).merge(children: [], sections:)
|
||||
end
|
||||
|
||||
if memo.key?(tag_id)
|
||||
@@ -533,13 +549,10 @@ class PostsController < ApplicationController
|
||||
|
||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
||||
|
||||
memo[tag_id] = TagRepr.inline(tag, language_code:).merge(children:, sections:)
|
||||
memo[tag_id] = TagRepr.inline(tag).merge(children:, sections:)
|
||||
end
|
||||
|
||||
internal_tags = root_ids.filter_map { |id| build_node.call(id, []) }
|
||||
external_tags =
|
||||
post.external_tags.map { ExternalTagRepr.inline(_1).merge(children: [], sections: []) }
|
||||
internal_tags + external_tags
|
||||
root_ids.filter_map { |id| build_node.call(id, []) }
|
||||
end
|
||||
|
||||
def sibling_posts_by_parent parent_post_ids
|
||||
@@ -686,49 +699,43 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def editable_tag_names_from_version version
|
||||
version.tags_json
|
||||
.select { _1.key?('tag_id') }
|
||||
.map { Post.tag_snapshot_literal(_1) }
|
||||
.compact
|
||||
.sort
|
||||
version.tags.to_s.split.reject { |name| name.downcase.start_with?('nico:') }.sort
|
||||
end
|
||||
|
||||
def post_snapshot_from_record post, locale
|
||||
def post_snapshot_from_record post
|
||||
{ title: post.title,
|
||||
video_ms: post.video_ms,
|
||||
original_created_from: snapshot_time(post.original_created_from),
|
||||
original_created_before: snapshot_time(post.original_created_before),
|
||||
tag_names: editable_tag_names_from_post(post, locale.language_code),
|
||||
tag_names: editable_tag_names_from_post(post),
|
||||
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
|
||||
end
|
||||
|
||||
def editable_tag_names_from_post post, language_code
|
||||
def editable_tag_names_from_post post
|
||||
post
|
||||
.post_tags
|
||||
.joins(tag: :tag_names)
|
||||
.kept
|
||||
.joins(tag: :tag_name)
|
||||
.merge(Tag.not_nico)
|
||||
.merge(Tag.where(deprecated_at: nil))
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.order('tag_names.name')
|
||||
.map do |post_tag|
|
||||
name = post_tag.tag.name(language_code)
|
||||
name = post_tag.tag.tag_name.name
|
||||
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||
|
||||
next name if sections.empty?
|
||||
|
||||
"#{ name }#{ sections.map { Post.section_literal(_1) }.join }"
|
||||
end.compact.sort
|
||||
end
|
||||
end
|
||||
|
||||
def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:,
|
||||
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
||||
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
||||
validate_original_created_values!(original_created_from, original_created_before)
|
||||
|
||||
Tag.normalise_tags!(locale, tag_names,
|
||||
with_tagme: false,
|
||||
with_no_deerjikist: false,
|
||||
deny_deprecated: true,
|
||||
with_sections: true) => { tags:, sections: }
|
||||
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||
with_sections: true) =>
|
||||
{ tags:, sections: }
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:)
|
||||
@@ -739,7 +746,7 @@ class PostsController < ApplicationController
|
||||
original_created_from: snapshot_time(original_created_from),
|
||||
original_created_before: snapshot_time(original_created_before),
|
||||
tag_names: tags.uniq(&:id).map { |tag|
|
||||
"#{ tag.name(locale.language_code) }#{ sections[tag.id].to_a.map { section_literal(_1) }.join }"
|
||||
"#{ tag.name }#{ sections[tag.id].to_a.map { section_literal(_1) }.join }"
|
||||
}.sort,
|
||||
parent_post_ids: parent_post_ids.sort }
|
||||
end
|
||||
@@ -863,7 +870,7 @@ class PostsController < ApplicationController
|
||||
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
|
||||
end
|
||||
|
||||
def apply_post_snapshot! locale, post, snapshot
|
||||
def apply_post_snapshot! post, snapshot
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
||||
|
||||
post.update!(title: snapshot[:title],
|
||||
@@ -871,13 +878,15 @@ class PostsController < ApplicationController
|
||||
original_created_from: snapshot[:original_created_from],
|
||||
original_created_before: snapshot[:original_created_before])
|
||||
|
||||
Tag.normalise_tags!(locale, snapshot[:tag_names],
|
||||
with_tagme: false,
|
||||
Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false,
|
||||
deny_deprecated: true,
|
||||
with_sections: true) => { tags:, sections: }
|
||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
with_sections: true) =>
|
||||
{ tags: editable_tags, sections: }
|
||||
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
|
||||
|
||||
readonly_tags = post.tags.nico.to_a
|
||||
|
||||
tags = readonly_tags + editable_tags
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
|
||||
post.video_ms = tags.any? { _1.id == Tag.video.id } ? snapshot[:video_ms] : nil
|
||||
|
||||
@@ -10,6 +10,7 @@ class TagChildrenController < ApplicationController
|
||||
|
||||
parent = Tag.find(parent_id)
|
||||
child = Tag.find(child_id)
|
||||
return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico?
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
TagVersioning.ensure_snapshot!(child, created_by_user: current_user)
|
||||
@@ -32,6 +33,7 @@ class TagChildrenController < ApplicationController
|
||||
|
||||
parent = Tag.find(parent_id)
|
||||
child = Tag.find(child_id)
|
||||
return render_bad_request('ニコニコ・タグの階層は変更できません.') if parent.nico? || child.nico?
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
TagVersioning.ensure_snapshot!(child, created_by_user: current_user)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class TagVersionsController < ApplicationController
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
tag_id = params[:id].presence
|
||||
page = (params[:page].presence || 1).to_i
|
||||
limit = (params[:limit].presence || 20).to_i
|
||||
@@ -19,8 +18,7 @@ class TagVersionsController < ApplicationController
|
||||
SQL
|
||||
.select('tag_versions.*', 'prev.name AS prev_name', 'prev.category AS prev_category',
|
||||
'prev.deprecated_at AS prev_deprecated_at',
|
||||
'prev.aliases AS prev_aliases', 'prev.names AS prev_names',
|
||||
'prev.parent_tag_ids AS prev_parent_tag_ids')
|
||||
'prev.aliases AS prev_aliases', 'prev.parent_tag_ids AS prev_parent_tag_ids')
|
||||
q = q.where('tag_versions.tag_id = ?', tag_id) if tag_id
|
||||
|
||||
count = q.except(:select, :order, :limit, :offset).count
|
||||
@@ -29,20 +27,18 @@ class TagVersionsController < ApplicationController
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
render json: { versions: serialise_versions(versions, locale.language_code), count: }
|
||||
render json: { versions: serialise_versions(versions), count: }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialise_versions rows, language_code
|
||||
def serialise_versions rows
|
||||
user_ids = rows.map(&:created_by_user_id).compact.uniq
|
||||
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
|
||||
|
||||
rows.map do |row|
|
||||
current_names = names_for_locale(row.names, language_code)
|
||||
previous_names = names_for_locale(row.attributes['prev_names'], language_code)
|
||||
cur_aliases = current_names.reject { _1['primary_flg'] }.map { _1['name'] }
|
||||
prev_aliases = previous_names.reject { _1['primary_flg'] }.map { _1['name'] }
|
||||
cur_aliases = split_values(row.aliases)
|
||||
prev_aliases = split_values(row.attributes['prev_aliases'])
|
||||
|
||||
cur_parent_tag_ids = split_parent_tag_ids(row.parent_tag_ids)
|
||||
prev_parent_tag_ids = split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])
|
||||
@@ -51,23 +47,21 @@ class TagVersionsController < ApplicationController
|
||||
|
||||
tags_by_id =
|
||||
Tag
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.includes(:tag_name, :materials, { tag_name: :wiki_page })
|
||||
.where(id: all_parent_tag_ids)
|
||||
.index_by(&:id)
|
||||
|
||||
parent_tags =
|
||||
build_version_values(cur_parent_tag_ids, prev_parent_tag_ids, key: :tag_id)
|
||||
.map do |h|
|
||||
{ tag: TagRepr.base(tags_by_id[h[:tag_id]], language_code:),
|
||||
{ tag: TagRepr.base(tags_by_id[h[:tag_id]]),
|
||||
type: h[:type] }
|
||||
end
|
||||
|
||||
{ tag_id: row.tag_id,
|
||||
version_no: row.version_no,
|
||||
event_type: row.event_type,
|
||||
names: row.names,
|
||||
name: { current: current_names.find { _1['primary_flg'] }&.fetch('name'),
|
||||
prev: previous_names.find { _1['primary_flg'] }&.fetch('name') },
|
||||
name: { current: row.name, prev: row.attributes['prev_name'] },
|
||||
category: { current: row.category, prev: row.attributes['prev_category'] },
|
||||
deprecated_at: { current: row.deprecated_at&.iso8601,
|
||||
prev: row.attributes['prev_deprecated_at']&.iso8601 },
|
||||
@@ -97,12 +91,5 @@ class TagVersionsController < ApplicationController
|
||||
|
||||
def split_values(values) = values.to_s.split(/\s+/).reject(&:blank?)
|
||||
|
||||
def names_for_locale names, language_code
|
||||
values = names.is_a?(String) ? JSON.parse(names) : Array(names)
|
||||
values.select { _1['language_code'] == language_code }
|
||||
rescue JSON::ParserError
|
||||
[]
|
||||
end
|
||||
|
||||
def split_parent_tag_ids(values) = split_values(values).map(&:to_i)
|
||||
end
|
||||
|
||||
@@ -5,8 +5,6 @@ require 'set'
|
||||
|
||||
class TagsController < ApplicationController
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
|
||||
post_id = params[:post]
|
||||
|
||||
name = params[:name].presence
|
||||
@@ -36,161 +34,49 @@ class TagsController < ApplicationController
|
||||
|
||||
offset = (page - 1) * limit
|
||||
|
||||
tags =
|
||||
q =
|
||||
if post_id.present?
|
||||
Tag
|
||||
.joins(:posts, :tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
posts: { id: post_id })
|
||||
Tag.joins(:posts, :tag_name)
|
||||
else
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true })
|
||||
end
|
||||
|
||||
external_tags =
|
||||
if post_id.present?
|
||||
ExternalTag.joins(:posts).where(posts: { id: post_id })
|
||||
else
|
||||
ExternalTag.all
|
||||
end
|
||||
|
||||
if name
|
||||
tags = tags.where('tag_names.name LIKE ?', "%#{ name }%")
|
||||
external_tags =
|
||||
external_tags.where("CONCAT(external_tags.platform, ':', external_tags.name) LIKE ?",
|
||||
"%#{ name }%")
|
||||
end
|
||||
|
||||
if category == 'nico'
|
||||
tags = tags.none
|
||||
elsif category
|
||||
tags = tags.where(category:)
|
||||
external_tags = external_tags.none
|
||||
end
|
||||
|
||||
if post_count_between[0]
|
||||
tags = tags.where('tags.post_count >= ?', post_count_between[0])
|
||||
external_tags = external_tags.where('external_tags.post_count >= ?', post_count_between[0])
|
||||
end
|
||||
|
||||
if post_count_between[1]
|
||||
tags = tags.where('tags.post_count <= ?', post_count_between[1])
|
||||
external_tags = external_tags.where('external_tags.post_count <= ?', post_count_between[1])
|
||||
end
|
||||
|
||||
if created_between[0]
|
||||
tags = tags.where('tags.created_at >= ?', created_between[0])
|
||||
external_tags = external_tags.where('external_tags.created_at >= ?', created_between[0])
|
||||
end
|
||||
|
||||
if created_between[1]
|
||||
tags = tags.where('tags.created_at <= ?', created_between[1])
|
||||
external_tags = external_tags.where('external_tags.created_at <= ?', created_between[1])
|
||||
end
|
||||
|
||||
if updated_between[0]
|
||||
tags = tags.where('tags.updated_at >= ?', updated_between[0])
|
||||
external_tags = external_tags.where('external_tags.created_at >= ?', updated_between[0])
|
||||
end
|
||||
|
||||
if updated_between[1]
|
||||
tags = tags.where('tags.updated_at <= ?', updated_between[1])
|
||||
external_tags = external_tags.where('external_tags.created_at <= ?', updated_between[1])
|
||||
Tag.joins(:tag_name)
|
||||
end
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
q = q.where(posts: { id: post_id }) if post_id.present?
|
||||
|
||||
q = q.where('tag_names.name LIKE ?', "%#{ name }%") if name
|
||||
q = q.where(category:) if category
|
||||
q = q.where('tags.post_count >= ?', post_count_between[0]) if post_count_between[0]
|
||||
q = q.where('tags.post_count <= ?', post_count_between[1]) if post_count_between[1]
|
||||
q = q.where('tags.created_at >= ?', created_between[0]) if created_between[0]
|
||||
q = q.where('tags.created_at <= ?', created_between[1]) if created_between[1]
|
||||
q = q.where('tags.updated_at >= ?', updated_between[0]) if updated_between[0]
|
||||
q = q.where('tags.updated_at <= ?', updated_between[1]) if updated_between[1]
|
||||
if deprecated_given
|
||||
if deprecated
|
||||
tags = tags.where.not(deprecated_at: nil)
|
||||
external_tags = external_tags.none
|
||||
else
|
||||
tags = tags.where(deprecated_at: nil)
|
||||
q = deprecated ? q.where.not(deprecated_at: nil) : q.where(deprecated_at: nil)
|
||||
end
|
||||
end
|
||||
|
||||
tag_sql = tags.select("'tag' AS source",
|
||||
'tags.id',
|
||||
'tag_names.name',
|
||||
'tags.category',
|
||||
'tags.post_count',
|
||||
'tags.created_at',
|
||||
'tags.updated_at',
|
||||
'tags.deprecated_at').to_sql
|
||||
|
||||
external_tag_sql = external_tags.select(
|
||||
"'external_tag' AS source",
|
||||
'external_tags.id',
|
||||
"CONCAT(external_tags.platform, ':', external_tags.name) AS name",
|
||||
"'nico' AS category",
|
||||
'external_tags.post_count',
|
||||
'external_tags.created_at',
|
||||
'external_tags.created_at AS updated_at',
|
||||
'NULL AS deprecated_at').to_sql
|
||||
|
||||
union_sql = "#{ tag_sql } UNION ALL #{ external_tag_sql }"
|
||||
|
||||
sort_sql =
|
||||
if order[0] == 'category'
|
||||
'CASE category ' +
|
||||
case order[0]
|
||||
when 'name'
|
||||
'tag_names.name'
|
||||
when 'category'
|
||||
'CASE tags.category ' +
|
||||
"WHEN 'deerjikist' THEN 0 " +
|
||||
"WHEN 'meme' THEN 1 " +
|
||||
"WHEN 'character' THEN 2 " +
|
||||
"WHEN 'general' THEN 3 " +
|
||||
"WHEN 'material' THEN 4 " +
|
||||
"WHEN 'meta' THEN 5 " +
|
||||
"WHEN 'nico' THEN 6 " +
|
||||
'END'
|
||||
"WHEN 'nico' THEN 6 END"
|
||||
else
|
||||
order[0]
|
||||
"tags.#{ order[0] }"
|
||||
end
|
||||
tags = q.order(Arel.sql("#{ sort_sql } #{ order[1] }, tags.id #{ order[1] }"))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.to_a
|
||||
|
||||
connection = ApplicationRecord.connection
|
||||
|
||||
count = connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
(#{ union_sql }) legacy_tags
|
||||
SQL
|
||||
|
||||
rows = connection.select_all(<<~SQL).to_a
|
||||
SELECT
|
||||
source
|
||||
, id
|
||||
FROM
|
||||
(#{ union_sql }) legacy_tags
|
||||
ORDER BY
|
||||
#{ sort_sql } #{ order[1] }
|
||||
, id #{ order[1] }
|
||||
, source #{ order[1] }
|
||||
LIMIT
|
||||
#{ limit }
|
||||
OFFSET
|
||||
#{ offset }
|
||||
SQL
|
||||
|
||||
tag_ids = rows.filter { _1['source'] == 'tag' }.map { _1['id'] }
|
||||
external_tag_ids = rows.filter { _1['source'] == 'external_tag' }.map { _1['id'] }
|
||||
|
||||
tags_by_id =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
id: tag_ids)
|
||||
.index_by(&:id)
|
||||
external_tags_by_id =
|
||||
ExternalTag
|
||||
.where(id: external_tag_ids)
|
||||
.index_by(&:id)
|
||||
|
||||
render json: { tags: rows.map { |row|
|
||||
if row['source'] == 'tag'
|
||||
TagRepr.base(tags_by_id.fetch(row['id']), language_code: locale.language_code)
|
||||
else
|
||||
external_tag_json(external_tags_by_id.fetch(row['id']))
|
||||
end
|
||||
}, count: }
|
||||
render json: { tags: TagRepr.many(tags), count: q.size }
|
||||
end
|
||||
|
||||
def with_depth
|
||||
@@ -198,8 +84,7 @@ class TagsController < ApplicationController
|
||||
parent_tag_id = nil if parent_tag_id <= 0
|
||||
material_filter = material_filter_param(default: 'any')
|
||||
|
||||
locale = resolve_locale!
|
||||
graph = build_with_depth_graph(material_filter, locale.language_code)
|
||||
graph = build_with_depth_graph(material_filter)
|
||||
|
||||
tag_ids =
|
||||
if parent_tag_id
|
||||
@@ -214,223 +99,137 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
def autocomplete
|
||||
locale = resolve_locale!
|
||||
|
||||
q = params[:q].to_s.strip.sub(/\Anot:/i, '')
|
||||
prefix = "#{ ActiveRecord::Base.sanitize_sql_like(q) }%"
|
||||
|
||||
with_nico = bool?(:nico, default: true)
|
||||
present_only = bool?(:present, default: true)
|
||||
|
||||
alias_rows =
|
||||
TagName
|
||||
.where(language_code: locale.language_code, primary_flg: false)
|
||||
.where('name LIKE ?', prefix)
|
||||
.pluck(:tag_id, :name)
|
||||
.where('name LIKE ?', "#{ q }%")
|
||||
.where.not(canonical_id: nil)
|
||||
.pluck(:canonical_id, :name)
|
||||
|
||||
matched_alias_by_tag_id = { }
|
||||
tag_ids = []
|
||||
alias_rows.each do |tag_id, alias_name|
|
||||
tag_ids << tag_id
|
||||
matched_alias_by_tag_id[tag_id] ||= alias_name
|
||||
matched_alias_by_tag_name_id = { }
|
||||
canonical_ids = []
|
||||
|
||||
alias_rows.each do |canonical_id, alias_name|
|
||||
canonical_ids << canonical_id
|
||||
matched_alias_by_tag_name_id[canonical_id] ||= alias_name
|
||||
end
|
||||
|
||||
base =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
deprecated_at: nil)
|
||||
base = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.where(deprecated_at: nil)
|
||||
base = base.where('tags.post_count > 0') if present_only
|
||||
|
||||
canonical_hit = base.where('tag_names.name LIKE ?', prefix)
|
||||
canonical_hit =
|
||||
base
|
||||
.where(((with_nico ? '(tags.category = ? AND tag_names.name LIKE ?) OR ' : '') +
|
||||
'tag_names.name LIKE ?'),
|
||||
*(with_nico ? ['nico', "nico:#{ q }%"] : []), "#{ q }%")
|
||||
|
||||
internal_tags = canonical_hit.or(base.where(id: tag_ids.uniq))
|
||||
tags =
|
||||
if canonical_ids.present?
|
||||
canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq))
|
||||
else
|
||||
canonical_hit
|
||||
end
|
||||
|
||||
internal_rows =
|
||||
internal_tags
|
||||
.order(Arel.sql('tags.post_count DESC, tag_names.name'))
|
||||
.limit(20)
|
||||
.map { |tag|
|
||||
TagRepr.base(tag, language_code: locale.language_code)
|
||||
.merge(matched_alias: matched_alias_by_tag_id[tag.id])
|
||||
tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a
|
||||
|
||||
render json: tags.map { |tag|
|
||||
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
|
||||
}
|
||||
|
||||
return render json: internal_rows unless with_nico
|
||||
|
||||
external_base = ExternalTag.all
|
||||
external_base = external_base.where('post_count > 0') if present_only
|
||||
|
||||
external_rows =
|
||||
external_base
|
||||
.where("CONCAT(platform, ':', name) LIKE ? OR name LIKE ?", prefix, prefix)
|
||||
.order(post_count: :desc, name: :asc)
|
||||
.limit(20)
|
||||
.map { external_tag_json(_1) }
|
||||
|
||||
rows =
|
||||
(internal_rows + external_rows)
|
||||
.sort_by { |row| [-row['post_count'], row['name'] || row[:name]] }
|
||||
.first(20)
|
||||
|
||||
render json: rows
|
||||
end
|
||||
|
||||
def show
|
||||
locale = resolve_locale!
|
||||
|
||||
tag =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.find_by(id: params[:id],
|
||||
tag_names: { language_code: locale.language_code, primary_flg: true })
|
||||
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
|
||||
|
||||
external_tag = ExternalTag.find_by(id: params[:id])
|
||||
return render json: ExternalTagRepr.base(external_tag) if external_tag
|
||||
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.find_by(id: params[:id])
|
||||
if tag
|
||||
render json: TagRepr.base(tag)
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def show_by_name
|
||||
name = params[:name].to_s.strip
|
||||
return render_bad_request('name は必須です.') if name.blank?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
tag =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
|
||||
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
|
||||
|
||||
platform, external_name = name.split(':', 2)
|
||||
return head :not_found unless external_name
|
||||
|
||||
external_tag = ExternalTag.find_by(platform:, name: external_name)
|
||||
return head :not_found unless external_tag
|
||||
|
||||
render json: ExternalTagRepr.base(external_tag)
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.find_by(tag_names: { name: })
|
||||
if tag
|
||||
render json: TagRepr.base(tag)
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def deerjikists
|
||||
locale = resolve_locale!
|
||||
|
||||
tag =
|
||||
Tag.joins(:tag_names)
|
||||
.includes(:tag_names, tag_names: :wiki_page)
|
||||
.find_by(id: params[:id],
|
||||
tag_names: { language_code: locale.language_code, primary_flg: true })
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, tag_name: :wiki_page)
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless tag
|
||||
|
||||
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
|
||||
deerjikists: DeerjikistRepr.many(
|
||||
tag.deerjikists,
|
||||
language_code: locale.language_code) }
|
||||
render json: { tag: TagRepr.base(tag),
|
||||
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
|
||||
end
|
||||
|
||||
def deerjikists_by_name
|
||||
name = params[:name].to_s.strip
|
||||
return render_bad_request('name は必須です.') if name.blank?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
tag =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, tag_names: :wiki_page)
|
||||
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, tag_name: :wiki_page)
|
||||
.find_by(tag_names: { name: })
|
||||
return head :not_found unless tag
|
||||
|
||||
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
|
||||
deerjikists: DeerjikistRepr.many(
|
||||
tag.deerjikists,
|
||||
language_code: locale.language_code) }
|
||||
render json: { tag: TagRepr.base(tag),
|
||||
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
|
||||
end
|
||||
|
||||
def update_deerjikists
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
tag =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, tag_names: :wiki_page)
|
||||
.find_by(id: params[:id],
|
||||
tag_names: { language_code: locale.language_code, primary_flg: true })
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, tag_name: :wiki_page)
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless tag
|
||||
|
||||
rows = normalise_deerjikist_rows(tag)
|
||||
return if performed?
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
tag.lock!
|
||||
|
||||
requested_keys = rows.map { |row| [row[:platform], row[:code]] }.uniq
|
||||
row_indexes_by_key = rows_by_key(rows)
|
||||
locked_deerjikists = lock_deerjikists_for_tag_update(tag.id, requested_keys)
|
||||
current_deerjikists = locked_deerjikists.filter { |deerjikist|
|
||||
deerjikist.tag_id == tag.id
|
||||
}
|
||||
requested_deerjikists = locked_deerjikists.filter { |deerjikist|
|
||||
row_indexes_by_key.key?([deerjikist.platform, deerjikist.code])
|
||||
}
|
||||
|
||||
render_deerjikist_conflicts(locale, requested_deerjikists, row_indexes_by_key, tag)
|
||||
raise ActiveRecord::Rollback if performed?
|
||||
|
||||
requested_keys_set = requested_keys.to_set
|
||||
current_deerjikists.each do |deerjikist|
|
||||
key = [deerjikist.platform, deerjikist.code]
|
||||
deerjikist.destroy! unless requested_keys_set.include?(key)
|
||||
end
|
||||
|
||||
existing_keys = requested_deerjikists.to_h { |deerjikist|
|
||||
[[deerjikist.platform, deerjikist.code], true]
|
||||
}
|
||||
requested_keys.each do |platform, code|
|
||||
next if existing_keys[[platform, code]]
|
||||
|
||||
deerjikist = Deerjikist.new(platform:, code:, tag:)
|
||||
row_index = row_indexes_by_key[[platform, code]].first
|
||||
begin
|
||||
render_deerjikist_form_record_invalid(deerjikist, row_index) unless deerjikist.save
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
conflicts = lock_deerjikists_for_tag_update(tag.id, [[platform, code]])
|
||||
render_deerjikist_conflicts(locale, conflicts, row_indexes_by_key, tag)
|
||||
end
|
||||
tag.deerjikists = []
|
||||
params[:_json].each.with_index do |item, i|
|
||||
platform = item[:platform]
|
||||
code = normalise_deerjikist_code(platform, item[:code])
|
||||
deerjikist = Deerjikist.find_or_initialize_by(platform:, code:)
|
||||
deerjikist.tag = tag
|
||||
render_deerjikist_form_record_invalid(deerjikist, i) unless deerjikist.save
|
||||
raise ActiveRecord::Rollback if performed?
|
||||
end
|
||||
end
|
||||
|
||||
return if performed?
|
||||
|
||||
render json: DeerjikistRepr.many(tag.reload.deerjikists,
|
||||
language_code: locale.language_code)
|
||||
render json: DeerjikistRepr.many(tag.reload.deerjikists)
|
||||
end
|
||||
|
||||
def materials_by_name
|
||||
name = params[:name].to_s.strip
|
||||
return render_bad_request('name は必須です.') if name.blank?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
material_filter = material_filter_param(default: 'any')
|
||||
|
||||
tag =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.find_by(tag_names: { name: })
|
||||
return head :not_found unless tag
|
||||
|
||||
graph = build_with_depth_graph(material_filter, locale.language_code)
|
||||
graph = build_with_depth_graph(material_filter)
|
||||
|
||||
render json: build_tag_children(tag, graph:, language_code: locale.language_code)
|
||||
render json: build_tag_children(tag, graph:)
|
||||
end
|
||||
|
||||
def update_all
|
||||
@@ -446,38 +245,39 @@ class TagsController < ApplicationController
|
||||
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
|
||||
return render_unprocessable_entity '廃止状態は必須です.', field: :deprecated unless params.key?(:deprecated)
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
return unless validate_tag_rename(locale, tag, name)
|
||||
return unless validate_tag_rename(tag, name)
|
||||
|
||||
alias_names = params[:aliases].to_s.split.uniq
|
||||
parent_names = params[:parent_tags].to_s.split.uniq
|
||||
deprecated = bool?(:deprecated)
|
||||
|
||||
if category == 'nico'
|
||||
if tag.nico? && deprecated
|
||||
return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated
|
||||
end
|
||||
|
||||
if tag.nico? || category == 'nico'
|
||||
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
||||
end
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||
|
||||
old_name = tag.name(locale.language_code)
|
||||
old_name = tag.name
|
||||
name_changed = name != old_name
|
||||
wiki_page = tag.tag_name(locale.language_code)&.wiki_page if name_changed
|
||||
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||
|
||||
if tag.deprecated? == deprecated
|
||||
tag.update!(category:)
|
||||
else
|
||||
tag.update!(category:, deprecated_at: deprecated ? Time.current : nil)
|
||||
end
|
||||
rename_tag_name!(locale, tag, name) if name_changed
|
||||
rename_tag_name!(tag, name) if name_changed
|
||||
|
||||
alias_names << old_name if name_changed && old_name.present?
|
||||
alias_names << old_name if name_changed
|
||||
alias_names.delete(name)
|
||||
|
||||
update_aliases!(locale, tag, alias_names)
|
||||
update_parent_tags!(locale, tag, parent_names)
|
||||
update_aliases!(tag, alias_names)
|
||||
update_parent_tags!(tag, parent_names)
|
||||
|
||||
tag.reload
|
||||
|
||||
@@ -485,20 +285,17 @@ class TagsController < ApplicationController
|
||||
tag,
|
||||
event_type: :update,
|
||||
created_by_user: current_user,
|
||||
language_code: locale.language_code,
|
||||
name_changed:,
|
||||
wiki_page:)
|
||||
end
|
||||
|
||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
||||
render json: TagRepr.base(tag.reload)
|
||||
end
|
||||
|
||||
def update
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
name = params[:name].presence
|
||||
category = params[:category].presence
|
||||
deprecated_given = params.key?(:deprecated)
|
||||
@@ -506,21 +303,24 @@ class TagsController < ApplicationController
|
||||
|
||||
tag = Tag.find(params[:id])
|
||||
|
||||
return unless validate_tag_rename(locale, tag, name)
|
||||
if tag.nico? && deprecated_given && deprecated
|
||||
return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated
|
||||
end
|
||||
|
||||
if category.present? && category == 'nico'
|
||||
return unless validate_tag_rename(tag, name)
|
||||
|
||||
if tag.nico? || (category.present? && category == 'nico')
|
||||
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
||||
end
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||
|
||||
old_name = tag.name(locale.language_code)
|
||||
old_name = tag.name
|
||||
name_changed = name.present? && name != old_name
|
||||
wiki_page = tag.tag_name(locale.language_code)&.wiki_page if name_changed
|
||||
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||
|
||||
rename_tag_name!(locale, tag, name) if name_changed
|
||||
rename_tag_name!(tag, name) if name_changed
|
||||
tag.update!(category:) if category.present?
|
||||
if deprecated_given && tag.deprecated? != deprecated
|
||||
tag.update!(deprecated_at: deprecated ? Time.current : nil)
|
||||
@@ -532,12 +332,11 @@ class TagsController < ApplicationController
|
||||
tag,
|
||||
event_type: :update,
|
||||
created_by_user: current_user,
|
||||
language_code: locale.language_code,
|
||||
name_changed:,
|
||||
wiki_page:)
|
||||
end
|
||||
|
||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
||||
render json: TagRepr.base(tag.reload)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -549,7 +348,7 @@ class TagsController < ApplicationController
|
||||
value
|
||||
end
|
||||
|
||||
def build_with_depth_graph material_filter, language_code
|
||||
def build_with_depth_graph material_filter
|
||||
children_by_parent_id = Hash.new { |h, k| h[k] = [] }
|
||||
parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] }
|
||||
|
||||
@@ -564,11 +363,8 @@ class TagsController < ApplicationController
|
||||
|
||||
material_tag_ids = Material.unscoped.kept.where.not(tag_id: nil).distinct.pluck(:tag_id).to_set
|
||||
|
||||
tags_by_id =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(id: tag_ids,
|
||||
tag_names: { language_code:, primary_flg: true })
|
||||
tags_by_id = Tag.joins(:tag_name)
|
||||
.where(id: tag_ids)
|
||||
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
||||
.each_with_object({ }) do |(id, name, category, deprecated_at), h|
|
||||
h[id] = { name:, category:, deprecated: deprecated_at.present?,
|
||||
@@ -702,33 +498,34 @@ class TagsController < ApplicationController
|
||||
children: [] }
|
||||
end
|
||||
|
||||
def build_tag_children tag, graph: nil, language_code: 'ja'
|
||||
def build_tag_children tag, graph: nil
|
||||
material = tag.materials.first
|
||||
tag_graph = graph && graph[:tags_by_id][tag.id]
|
||||
material =
|
||||
nil if tag_graph && !with_depth_visible_tag?(tag_graph, graph[:material_filter])
|
||||
|
||||
children = tag.children.sort_by { _1.name(language_code) }
|
||||
children = tag.children.sort_by(&:name)
|
||||
if graph
|
||||
children = children.filter { |child_tag| visible_subtree?(child_tag.id, graph) }
|
||||
end
|
||||
|
||||
TagRepr.base(tag, language_code:).merge(
|
||||
children: children.map { build_tag_children(_1, graph:, language_code:) },
|
||||
TagRepr.base(tag).merge(
|
||||
children: children.map { build_tag_children(_1, graph:) },
|
||||
has_material: tag_graph ? tag_graph[:has_material] : material.present?,
|
||||
material: material && MaterialRepr.base(
|
||||
material,
|
||||
host: request.base_url,
|
||||
language_code:))
|
||||
material: material && MaterialRepr.base(material, host: request.base_url))
|
||||
end
|
||||
|
||||
def record_tag_version! tag, event_type:, created_by_user:, language_code: 'ja',
|
||||
name_changed: false, wiki_page: nil
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
|
||||
def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil
|
||||
if tag.nico?
|
||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
return
|
||||
end
|
||||
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
|
||||
return unless name_changed
|
||||
|
||||
wiki_page ||= tag.tag_name('ja')&.wiki_page
|
||||
wiki_page ||= tag.tag_name.wiki_page
|
||||
return unless wiki_page&.wiki_versions&.exists?
|
||||
|
||||
WikiVersionRecorder.record!(
|
||||
@@ -737,134 +534,109 @@ class TagsController < ApplicationController
|
||||
created_by_user:)
|
||||
end
|
||||
|
||||
def validate_tag_rename locale, tag, name
|
||||
return true if name.blank? || name == tag.name(locale.language_code)
|
||||
def validate_tag_rename tag, name
|
||||
return true if name.blank? || name == tag.name
|
||||
|
||||
system_tag_names = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ']
|
||||
if TagName.exists?(tag_id: tag.id,
|
||||
language_code: 'ja',
|
||||
primary_flg: true,
|
||||
name: system_tag_names)
|
||||
if tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico])
|
||||
render_unprocessable_entity 'システム・タグの名称は変更できません.', field: :name
|
||||
return false
|
||||
end
|
||||
|
||||
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
||||
return true unless target_tag_name&.primary_flg
|
||||
target_tag_name = TagName.with_discarded.find_by(name:)
|
||||
return true if target_tag_name.nil?
|
||||
return true if target_tag_name.canonical_id?
|
||||
|
||||
render_unprocessable_entity 'その名前は既に使はれてゐます.', field: :name
|
||||
false
|
||||
end
|
||||
|
||||
def rename_tag_name! locale, tag, name
|
||||
return if name == tag.name(locale.language_code)
|
||||
def rename_tag_name! tag, name
|
||||
return if name == tag.name
|
||||
|
||||
current_tag_name = tag.tag_name(locale.language_code)
|
||||
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
||||
current_tag_name = tag.tag_name
|
||||
target_tag_name = TagName.with_discarded.find_by(name:)
|
||||
|
||||
unless target_tag_name
|
||||
if current_tag_name
|
||||
if target_tag_name.nil?
|
||||
current_tag_name.update!(name:)
|
||||
else
|
||||
current_tag_name = TagName.create!(
|
||||
tag:,
|
||||
language_code: locale.language_code,
|
||||
script_code: locale.script_code,
|
||||
name:,
|
||||
primary_flg: true)
|
||||
tag.association(:tag_names).reset
|
||||
end
|
||||
return
|
||||
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
|
||||
|
||||
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
|
||||
old_owner_tag = promoted_tag_name.tag
|
||||
old_owner_tag = promoted_tag_name.canonical&.tag
|
||||
|
||||
if old_owner_tag && old_owner_tag != tag
|
||||
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
|
||||
end
|
||||
|
||||
current_tag_name.wiki_page&.update!(tag_name_id: promoted_tag_name.id)
|
||||
current_tag_name.association(:wiki_page).reset
|
||||
promoted_tag_name.undiscard! if promoted_tag_name.discarded?
|
||||
promoted_tag_name.update!(canonical: nil)
|
||||
|
||||
current_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
|
||||
promoted_tag_name.update!(tag:, canonical_id: nil, primary_flg: true)
|
||||
|
||||
TagName.where(tag:,
|
||||
language_code: current_tag_name.language_code,
|
||||
primary_flg: false)
|
||||
TagName.with_discarded
|
||||
.where(canonical_id: current_tag_name.id)
|
||||
.where.not(id: promoted_tag_name.id)
|
||||
.find_each do |alias_tag_name|
|
||||
alias_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
|
||||
alias_tag_name.update!(canonical: promoted_tag_name)
|
||||
end
|
||||
|
||||
tag.update!(tag_name_id: promoted_tag_name.id)
|
||||
current_tag_name.wiki_page&.update!(tag_name: promoted_tag_name)
|
||||
tag.update!(tag_name: promoted_tag_name)
|
||||
current_tag_name.association(:wiki_page).reset
|
||||
current_tag_name.association(:tag).reset
|
||||
current_tag_name.reload.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
|
||||
current_tag_name.reload.update!(canonical: promoted_tag_name)
|
||||
|
||||
return unless old_owner_tag && old_owner_tag != tag
|
||||
|
||||
record_tag_version!(old_owner_tag.reload, event_type: :update,
|
||||
created_by_user: current_user,
|
||||
language_code: current_tag_name.language_code)
|
||||
record_tag_version!(old_owner_tag.reload, event_type: :update, created_by_user: current_user)
|
||||
end
|
||||
|
||||
def update_aliases! locale, tag, alias_names
|
||||
def update_aliases! tag, alias_names
|
||||
alias_names = alias_names.uniq
|
||||
|
||||
affected_tags = [tag]
|
||||
|
||||
primary_tag_name = TagName.find_by!(tag:,
|
||||
language_code: locale.language_code,
|
||||
primary_flg: true)
|
||||
current_aliases = tag.tag_names.where(language_code: locale.language_code,
|
||||
primary_flg: false).to_a
|
||||
current_aliases = tag.tag_name.aliases.to_a
|
||||
|
||||
current_aliases.each do |alias_tag_name|
|
||||
next if alias_names.include?(alias_tag_name.name)
|
||||
|
||||
affected_tags << alias_tag_name.tag
|
||||
affected_tags << alias_tag_name.canonical&.tag
|
||||
end
|
||||
|
||||
alias_names.each do |alias_name|
|
||||
alias_tag_name = TagName.find_by(language_code: locale.language_code, name: alias_name)
|
||||
affected_tags << alias_tag_name&.tag
|
||||
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
||||
affected_tags << alias_tag_name.canonical&.tag
|
||||
end
|
||||
|
||||
affected_tags.compact.uniq.each do |affected_tag|
|
||||
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user)
|
||||
end
|
||||
|
||||
current_aliases.each do |alias_tag_name|
|
||||
next if alias_names.include?(alias_tag_name.name)
|
||||
|
||||
alias_tag_name.update!(canonical_id: nil, tag_id: nil)
|
||||
alias_tag_name.update!(canonical: nil)
|
||||
end
|
||||
|
||||
alias_names.each do |alias_name|
|
||||
alias_tag_name = TagName.find_or_initialize_by(
|
||||
language_code: locale.language_code,
|
||||
name: alias_name)
|
||||
alias_tag_name.script_code = locale.script_code
|
||||
alias_tag_name.update!(tag:, primary_flg: false, canonical_id: primary_tag_name.id)
|
||||
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
||||
alias_tag_name.update!(canonical: tag.tag_name)
|
||||
end
|
||||
|
||||
affected_tags.compact.uniq.each do |affected_tag|
|
||||
record_tag_version!(affected_tag, event_type: :update,
|
||||
created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
record_tag_version!(affected_tag, event_type: :update, created_by_user: current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def update_parent_tags! locale, tag, parent_names
|
||||
parent_tags = Tag.normalise_tags!(locale, parent_names,
|
||||
with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
def update_parent_tags! tag, parent_names
|
||||
parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false,
|
||||
with_no_deerjikist: false,
|
||||
deny_nico: true)
|
||||
|
||||
old_parent_tags = tag.parents.to_a
|
||||
|
||||
@@ -881,7 +653,6 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
def normalise_deerjikist_code platform, code
|
||||
code = code.to_s
|
||||
return code if platform != 'youtube' || code[0] != '@'
|
||||
|
||||
url = "https://www.youtube.com/#{ code }"
|
||||
@@ -898,84 +669,6 @@ class TagsController < ApplicationController
|
||||
nil
|
||||
end
|
||||
|
||||
def normalise_deerjikist_rows tag
|
||||
rows = []
|
||||
|
||||
params[:_json].each.with_index do |item, index|
|
||||
platform = item[:platform]
|
||||
unless Deerjikist.platforms.key?(platform)
|
||||
render_deerjikist_platform_invalid(index)
|
||||
return rows
|
||||
end
|
||||
|
||||
code = normalise_deerjikist_code(platform, item[:code])
|
||||
deerjikist = Deerjikist.new(platform:, code:, tag:)
|
||||
unless deerjikist.valid?
|
||||
render_deerjikist_form_record_invalid(deerjikist, index)
|
||||
return rows
|
||||
end
|
||||
|
||||
rows << { index:, platform:, code: }
|
||||
end
|
||||
|
||||
rows
|
||||
end
|
||||
|
||||
def lock_deerjikists_for_tag_update tag_id, keys
|
||||
clauses = ['tag_id = ?']
|
||||
values = [tag_id]
|
||||
|
||||
keys.each do |platform, code|
|
||||
clauses << '(platform = ? AND code = ?)'
|
||||
values << platform << code
|
||||
end
|
||||
|
||||
Deerjikist
|
||||
.where(clauses.join(' OR '), *values)
|
||||
.order(:platform, :code)
|
||||
.lock
|
||||
.to_a
|
||||
end
|
||||
|
||||
def render_deerjikist_conflicts locale, deerjikists, row_indexes_by_key, tag
|
||||
conflicts = deerjikists.filter { |deerjikist| deerjikist.tag_id != tag.id }
|
||||
return if conflicts.empty?
|
||||
|
||||
tag_names_by_id =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
id: conflicts.map(&:tag_id).uniq)
|
||||
.pluck('tags.id', 'tag_names.name')
|
||||
.to_h
|
||||
fields = { }
|
||||
|
||||
conflicts.each do |deerjikist|
|
||||
message = "この情報は既に「#{ tag_names_by_id[deerjikist.tag_id] }」に紐づいてゐます."
|
||||
row_indexes_by_key[[deerjikist.platform, deerjikist.code]].each do |index|
|
||||
field = :"deerjikists.#{ index }.code"
|
||||
fields[field] ||= []
|
||||
fields[field] << message
|
||||
end
|
||||
end
|
||||
|
||||
render_validation_error fields:
|
||||
end
|
||||
|
||||
def render_deerjikist_platform_invalid index
|
||||
render_validation_error fields: {
|
||||
:"deerjikists.#{ index }.platform" => ['値が不正です.'],
|
||||
}
|
||||
end
|
||||
|
||||
def rows_by_key rows
|
||||
rows.each_with_object({ }) do |row, result|
|
||||
key = [row[:platform], row[:code]]
|
||||
result[key] ||= []
|
||||
result[key] << row[:index]
|
||||
end
|
||||
end
|
||||
|
||||
def render_deerjikist_form_record_invalid deerjikist, index
|
||||
fields = { }
|
||||
|
||||
@@ -994,20 +687,4 @@ class TagsController < ApplicationController
|
||||
|
||||
render_validation_error fields:
|
||||
end
|
||||
|
||||
def external_tag_json tag
|
||||
{ 'id' => tag.id,
|
||||
'name' => "#{ tag.platform }:#{ tag.name }",
|
||||
'category' => 'nico',
|
||||
'deprecated_at' => nil,
|
||||
'created_at' => tag.created_at,
|
||||
'updated_at' => tag.created_at,
|
||||
'post_count' => tag.post_count,
|
||||
'matched_alias' => nil,
|
||||
'aliases' => [],
|
||||
'parents' => [],
|
||||
'has_wiki' => false,
|
||||
'material_id' => nil,
|
||||
'has_deerjikists' => false }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class TheatreSkipEventsController < ApplicationController
|
||||
events =
|
||||
TheatreSkipEvent
|
||||
.where(theatre_id: params[:theatre_id])
|
||||
.includes(:post, tags: :tag_names)
|
||||
.includes(:post, tags: :tag_name)
|
||||
.order(created_at: :desc)
|
||||
.limit(limit)
|
||||
|
||||
@@ -14,7 +14,7 @@ class TheatreSkipEventsController < ApplicationController
|
||||
{ id: event.id,
|
||||
theatre_id: event.theatre_id,
|
||||
post: { id: event.post.id, title: event.post.title, url: event.post.url },
|
||||
tags: event.tags.map { |tag| { id: tag.id, name: tag.name('ja') } },
|
||||
tags: event.tags.map { |tag| { id: tag.id, name: tag.name } },
|
||||
programme_position: event.programme_position,
|
||||
created_at: event.created_at }
|
||||
}
|
||||
|
||||
@@ -94,11 +94,7 @@ class WikiPagesController < ApplicationController
|
||||
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
||||
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
||||
|
||||
tag_name = TagName.find_or_create_by!(language_code: 'ja', name: title) do |name|
|
||||
name.script_code = 'Jpan'
|
||||
name.primary_flg = true
|
||||
name.auto_generated = false
|
||||
end
|
||||
tag_name = TagName.find_undiscard_or_create_by!(name: title)
|
||||
|
||||
page =
|
||||
Wiki::Commit.create_content!(
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
class ExternalTag < ApplicationRecord
|
||||
enum :platform, nico: 'nico'
|
||||
validates :platform, presence: true, inclusion: { in: ExternalTag.platforms.keys }
|
||||
|
||||
has_many :post_external_tags, dependent: :delete_all
|
||||
has_many :posts, through: :post_external_tags
|
||||
|
||||
has_many :nico_tag_versions, foreign_key: :tag_id, inverse_of: :external_tag
|
||||
|
||||
has_many :nico_tag_relations,
|
||||
foreign_key: :nico_tag_id,
|
||||
inverse_of: :nico_tag,
|
||||
dependent: :destroy
|
||||
|
||||
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
||||
|
||||
def snapshot_linked_tag_names language_code = 'ja'
|
||||
linked_tags
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.order('tag_names.name')
|
||||
.pluck('tag_names.name')
|
||||
end
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
class Language < ApplicationRecord
|
||||
has_many :locales, foreign_key: :language_code, primary_key: :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('ja')),
|
||||
script_code:,
|
||||
primary_flg: true,
|
||||
# TODO: 公証実装したら書く.
|
||||
# auto_generated: true,
|
||||
canonical_id: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,24 @@
|
||||
class NicoTagRelation < ApplicationRecord
|
||||
belongs_to :nico_tag,
|
||||
class_name: 'ExternalTag',
|
||||
foreign_key: :nico_tag_id,
|
||||
inverse_of: :nico_tag_relations
|
||||
belongs_to :tag, class_name: 'Tag', foreign_key: :tag_id
|
||||
belongs_to :nico_tag, class_name: 'Tag'
|
||||
belongs_to :tag, class_name: 'Tag'
|
||||
|
||||
validates :nico_tag_id, presence: true
|
||||
validates :tag_id, presence: true
|
||||
|
||||
validate :nico_tag_must_be_nico
|
||||
validate :tag_mustnt_be_nico
|
||||
|
||||
private
|
||||
|
||||
def nico_tag_must_be_nico
|
||||
if nico_tag && nico_tag.category != 'nico'
|
||||
errors.add :nico_tag_id, 'タグのカテゴリがニコニコである必要があります.'
|
||||
end
|
||||
end
|
||||
|
||||
def tag_mustnt_be_nico
|
||||
if tag && tag.category == 'nico'
|
||||
errors.add :tag_id, '連携先タグのカテゴリはニコニコであってはなりません.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class NicoTagVersion < ApplicationRecord
|
||||
include VersionRecord
|
||||
|
||||
belongs_to :external_tag, foreign_key: :tag_id, inverse_of: :nico_tag_versions
|
||||
belongs_to :tag
|
||||
|
||||
validates :name, presence: true
|
||||
end
|
||||
|
||||
@@ -55,27 +55,25 @@ class Post < ApplicationRecord
|
||||
belongs_to :uploaded_user, class_name: 'User', optional: true
|
||||
|
||||
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 }) },
|
||||
through: :post_tags,
|
||||
source: :tag
|
||||
through: :active_post_tags, source: :tag
|
||||
|
||||
has_many :user_post_views, dependent: :delete_all
|
||||
has_many :post_similarities, dependent: :delete_all
|
||||
has_many :post_versions
|
||||
|
||||
has_many :gekanator_guessed_games,
|
||||
class_name: 'GekanatorGame',
|
||||
foreign_key: :guessed_post_id,
|
||||
dependent: :delete_all,
|
||||
inverse_of: :guessed_post
|
||||
|
||||
has_many :gekanator_correct_games,
|
||||
class_name: 'GekanatorGame',
|
||||
foreign_key: :correct_post_id,
|
||||
dependent: :delete_all,
|
||||
inverse_of: :correct_post
|
||||
|
||||
has_many :gekanator_question_examples, dependent: :delete_all
|
||||
|
||||
has_many :parent_post_implications,
|
||||
@@ -92,9 +90,6 @@ class Post < ApplicationRecord
|
||||
inverse_of: :parent_post
|
||||
has_many :children, through: :child_post_implications, source: :post
|
||||
|
||||
has_many :post_external_tags, dependent: :destroy
|
||||
has_many :external_tags, through: :post_external_tags
|
||||
|
||||
has_one_attached :thumbnail
|
||||
|
||||
attribute :version_no, :integer, default: 1
|
||||
@@ -126,14 +121,14 @@ class Post < ApplicationRecord
|
||||
super(options).merge(thumbnail: nil)
|
||||
end
|
||||
|
||||
def snapshot_tag_names language_code = 'ja'
|
||||
def snapshot_tag_names
|
||||
post_tags
|
||||
.joins(tag: :tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.kept
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.order('tag_names.name')
|
||||
.map do |post_tag|
|
||||
name = post_tag.tag.name(language_code)
|
||||
name = post_tag.tag.tag_name.name
|
||||
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||
|
||||
next name if sections.empty?
|
||||
@@ -142,42 +137,6 @@ class Post < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def self.tag_snapshot_literal tag
|
||||
sections = tag.fetch('sections', []).map do |sec|
|
||||
begin_ms = sec.fetch('begin_ms')
|
||||
end_ms = sec['end_ms']
|
||||
|
||||
"[#{ Post.ms_to_time(begin_ms) }-#{ end_ms ? Post.ms_to_time(end_ms) : '' }]"
|
||||
end
|
||||
|
||||
"#{ tag.fetch('name') }#{ sections.join }"
|
||||
end
|
||||
|
||||
def snapshot_tags_json language_code = 'ja'
|
||||
tag_snapshots =
|
||||
post_tags
|
||||
.joins(tag: :tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.order('tags.id')
|
||||
.map { |pt|
|
||||
{ 'tag_id' => pt.tag.id,
|
||||
'version_no' => pt.tag.version_no,
|
||||
'name' => pt.tag.name(language_code),
|
||||
'category' => pt.tag.category,
|
||||
'sections' => pt.sections.sort_by(&:begin_ms).map {
|
||||
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
|
||||
} }
|
||||
}
|
||||
|
||||
external_tag_snapshots =
|
||||
post_external_tags.order(:external_tag_id).map {
|
||||
{ 'external_tag_id' => _1.external_tag_id }
|
||||
}
|
||||
|
||||
tag_snapshots + external_tag_snapshots
|
||||
end
|
||||
|
||||
def self.section_literal section
|
||||
end_ms =
|
||||
section.end_ms ? Post.ms_to_time(section.end_ms) : ''
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
class PostExternalTag < ApplicationRecord
|
||||
belongs_to :post
|
||||
belongs_to :external_tag, counter_cache: :post_count
|
||||
end
|
||||
@@ -1,7 +1,14 @@
|
||||
class PostTag < ApplicationRecord
|
||||
include Discard::Model
|
||||
|
||||
before_destroy do
|
||||
raise ActiveRecord::ReadOnlyRecord, '消さないでください.'
|
||||
end
|
||||
|
||||
belongs_to :post
|
||||
belongs_to :tag, counter_cache: :post_count
|
||||
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',
|
||||
foreign_key: [:post_id, :tag_id],
|
||||
@@ -11,5 +18,18 @@ class PostTag < ApplicationRecord
|
||||
|
||||
validates :post_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
|
||||
|
||||
@@ -4,7 +4,7 @@ class PostTagSection < ApplicationRecord
|
||||
belongs_to :post
|
||||
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],
|
||||
inverse_of: :sections,
|
||||
optional: true
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
class Script < ApplicationRecord
|
||||
;
|
||||
end
|
||||
+69
-69
@@ -2,6 +2,8 @@ require 'set'
|
||||
|
||||
|
||||
class Tag < ApplicationRecord
|
||||
include MyDiscard
|
||||
|
||||
class NicoTagNormalisationError < ArgumentError
|
||||
;
|
||||
end
|
||||
@@ -26,12 +28,15 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
|
||||
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 :linked_tags, through: :nico_tag_relations, source: :tag
|
||||
|
||||
has_many :reversed_nico_tag_relations,
|
||||
class_name: 'NicoTagRelation',
|
||||
foreign_key: :tag_id,
|
||||
dependent: :destroy
|
||||
class_name: 'NicoTagRelation', foreign_key: :tag_id, dependent: :destroy
|
||||
has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag
|
||||
|
||||
has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy
|
||||
@@ -49,22 +54,32 @@ class Tag < ApplicationRecord
|
||||
has_many :materials
|
||||
|
||||
has_many :tag_versions
|
||||
has_many :nico_tag_versions
|
||||
|
||||
has_many :tag_names
|
||||
belongs_to :tag_name
|
||||
delegate :wiki_page, to: :tag_name
|
||||
|
||||
attribute :version_no, :integer, default: 1
|
||||
|
||||
delegate :name, to: :tag_name, allow_nil: true
|
||||
validates :tag_name, presence: true
|
||||
|
||||
enum :category, deerjikist: 'deerjikist',
|
||||
meme: 'meme',
|
||||
character: 'character',
|
||||
general: 'general',
|
||||
material: 'material',
|
||||
nico: 'nico',
|
||||
meta: 'meta'
|
||||
|
||||
validates :category, presence: true, inclusion: { in: Tag.categories.keys }
|
||||
|
||||
validate :nico_tag_name_must_start_with_nico
|
||||
validate :tag_name_must_be_canonical
|
||||
validate :category_must_be_deerjikist_with_deerjikists
|
||||
validate :tag_names_must_be_valid
|
||||
validate :nico_tags_cannot_be_deprecated
|
||||
|
||||
scope :nico_tags, -> { nico }
|
||||
|
||||
CATEGORY_PREFIXES = {
|
||||
'general:' => :general,
|
||||
@@ -78,20 +93,8 @@ class Tag < ApplicationRecord
|
||||
'mtr:' => :material,
|
||||
'meta:' => :meta }.freeze
|
||||
|
||||
SYSTEM_TAG_NAMES = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ', 'YouTube'].freeze
|
||||
|
||||
def tag_name language_code
|
||||
if tag_names.loaded?
|
||||
tag_names.find { _1.language_code == language_code && _1.primary_flg }
|
||||
else
|
||||
tag_names.find_by(language_code:, primary_flg: true)
|
||||
end
|
||||
end
|
||||
def name(language_code) = tag_name(language_code)&.name
|
||||
def wiki_page = tag_name('ja')&.wiki_page
|
||||
|
||||
def system?
|
||||
tag_name('ja')&.name.in?(SYSTEM_TAG_NAMES)
|
||||
def name= val
|
||||
(self.tag_name ||= build_tag_name).name = val
|
||||
end
|
||||
|
||||
def deprecated? = deprecated_at?
|
||||
@@ -102,20 +105,19 @@ class Tag < ApplicationRecord
|
||||
|
||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||
|
||||
def self.tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta)
|
||||
def self.bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
|
||||
def self.no_deerjikist =
|
||||
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
|
||||
def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
|
||||
def self.niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
|
||||
def self.youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
|
||||
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
|
||||
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
|
||||
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
|
||||
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
|
||||
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
|
||||
def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
|
||||
|
||||
def self.normalise_tags! locale, tag_names,
|
||||
with_tagme: true,
|
||||
def self.normalise_tags! tag_names, with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_nico: true,
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
end
|
||||
|
||||
@@ -141,11 +143,11 @@ class Tag < ApplicationRecord
|
||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
name = TagName.canonicalise(locale, 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?
|
||||
raise DeprecatedTagNormalisationError, [tag.name(locale.language_code)]
|
||||
raise DeprecatedTagNormalisationError, [tag.name]
|
||||
end
|
||||
|
||||
tag.update!(category: cat) if cat && tag.category != cat
|
||||
@@ -231,23 +233,13 @@ class Tag < ApplicationRecord
|
||||
[left_end_ms, right_end_ms].max
|
||||
end
|
||||
|
||||
def self.find_or_create_by_tag_name! locale, name, category:
|
||||
language_code = locale.language_code
|
||||
name = name.to_s.strip
|
||||
def self.find_or_create_by_tag_name! name, category:
|
||||
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
||||
tn = tn.canonical if tn.canonical_id?
|
||||
|
||||
tn = TagName.find_or_create_by!(language_code:, name:) do
|
||||
_1.script_code = locale.script_code
|
||||
_1.primary_flg = true
|
||||
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
|
||||
t.category = category
|
||||
end
|
||||
|
||||
tag = tn.tag
|
||||
return tag if tag
|
||||
|
||||
tag = Tag.create!(tag_name_id: tn.id, category:)
|
||||
tn.update!(tag:)
|
||||
tag.association(:tag_names).reset
|
||||
|
||||
tag
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
@@ -267,28 +259,30 @@ class Tag < ApplicationRecord
|
||||
|
||||
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
|
||||
affected_post_ids << post_id
|
||||
source_pt.destroy!
|
||||
unless PostTag.exists?(post_id:, tag: target_tag)
|
||||
source_pt.discard_by!(created_by_user)
|
||||
unless PostTag.kept.exists?(post_id:, tag: target_tag)
|
||||
PostTag.create!(post_id:, tag: target_tag)
|
||||
end
|
||||
end
|
||||
|
||||
source_tag_name = source_tag.tag_name('ja')
|
||||
source_tag_name = source_tag.tag_name
|
||||
|
||||
if source_tag_name.wiki_page.present?
|
||||
raise ActiveRecord::RecordInvalid.new(source_tag_name)
|
||||
end
|
||||
|
||||
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
|
||||
source_tag.tag_names.update_all(tag_id: target_tag.id, primary_flg: false,
|
||||
updated_at: Time.current)
|
||||
source_tag.destroy!
|
||||
source_tag.discard!
|
||||
|
||||
if source_tag.nico?
|
||||
source_tag_name.discard!
|
||||
else
|
||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||
updated_at: Time.current)
|
||||
end
|
||||
|
||||
TagVersioning.record!(target_tag, event_type: :update, created_by_user:)
|
||||
end
|
||||
@@ -299,33 +293,33 @@ class Tag < ApplicationRecord
|
||||
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
|
||||
|
||||
target_tag.reload
|
||||
end
|
||||
|
||||
def snapshot_aliases(language_code)
|
||||
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
|
||||
end
|
||||
def snapshot_aliases = tag_name.aliases.kept.order(:name).pluck(:name)
|
||||
|
||||
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
||||
|
||||
def snapshot_linked_tag_names language_code
|
||||
linked_tags
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.order('tag_names.name')
|
||||
.pluck('tag_names.name')
|
||||
def snapshot_linked_tag_names
|
||||
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_names_must_be_valid
|
||||
tag_names.each do |tag_name|
|
||||
next if tag_name.valid?
|
||||
def nico_tag_name_must_start_with_nico
|
||||
n = name.to_s
|
||||
if ((nico? && !(n.downcase.start_with?('nico:'))) ||
|
||||
(!(nico?) && n.downcase.start_with?('nico:')))
|
||||
errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
|
||||
tag_name.errors[:name].each { errors.add :name, _1 }
|
||||
def tag_name_must_be_canonical
|
||||
if tag_name&.canonical_id?
|
||||
errors.add :tag_name, 'tag_names へは実体を示す必要があります.'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -360,4 +354,10 @@ class Tag < ApplicationRecord
|
||||
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
def nico_tags_cannot_be_deprecated
|
||||
if nico? && deprecated_at.present?
|
||||
errors.add :deprecated_at, 'ニコタグは廃止できません.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,54 +1,45 @@
|
||||
class TagName < ApplicationRecord
|
||||
belongs_to :language, foreign_key: :language_code, primary_key: :code
|
||||
belongs_to :tag, optional: true
|
||||
include MyDiscard
|
||||
|
||||
has_one :tag
|
||||
has_one :wiki_page
|
||||
|
||||
validates :name, presence: true,
|
||||
length: { maximum: 255 },
|
||||
uniqueness: { scope: :language_code }
|
||||
belongs_to :canonical, class_name: 'TagName', optional: true
|
||||
has_many :aliases, class_name: 'TagName', foreign_key: :canonical_id
|
||||
|
||||
validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
|
||||
|
||||
validate :canonical_must_be_canonical
|
||||
validate :alias_name_must_not_have_prefix
|
||||
validate :alias_must_not_have_wiki_page
|
||||
validate :canonical_must_not_be_present_with_tag_or_wiki_page
|
||||
validate :name_must_be_sanitised
|
||||
validate :name_mustnt_start_with_nico
|
||||
|
||||
def primary? = primary_flg
|
||||
|
||||
def self.canonicalise locale, names
|
||||
def self.canonicalise names
|
||||
names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?)
|
||||
return [] if names.blank?
|
||||
|
||||
tns = TagName.where(language_code: locale.language_code, name: names).index_by(&:name)
|
||||
tns = TagName.includes(:canonical).where(name: names).index_by(&:name)
|
||||
|
||||
names.map { |name|
|
||||
tag_name = tns[name]
|
||||
if !tag_name || tag_name.primary?
|
||||
name
|
||||
else
|
||||
TagName.find_by(language_code: locale.language_code,
|
||||
tag_id: tag_name.tag_id,
|
||||
primary_flg: true)&.name || name
|
||||
end
|
||||
}.uniq
|
||||
end
|
||||
|
||||
def self.generate_name locale, tag, name
|
||||
# TODO: 言語ごとの自動命名ロジック完成したら書く.
|
||||
|
||||
"Tag_##{ tag.id }"
|
||||
names.map { |name| tns[name]&.canonical&.name || name }.uniq
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def canonical_must_be_canonical
|
||||
if canonical&.canonical_id?
|
||||
errors.add :canonical, 'canonical は実体を示す必要があります.'
|
||||
end
|
||||
end
|
||||
|
||||
def alias_name_must_not_have_prefix
|
||||
if !(primary?) && name.to_s.include?(':')
|
||||
if canonical_id? && name.to_s.include?(':')
|
||||
errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.'
|
||||
end
|
||||
end
|
||||
|
||||
def alias_must_not_have_wiki_page
|
||||
if !(primary?) && wiki_page
|
||||
errors.add :primary_flg, 'Wiki 参照がある名前はエーリアスになれません.'
|
||||
def canonical_must_not_be_present_with_tag_or_wiki_page
|
||||
if canonical_id? && (tag || wiki_page)
|
||||
errors.add :canonical, 'タグもしくは Wiki の参照がある名前はエーリアスになれません.'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -57,10 +48,4 @@ class TagName < ApplicationRecord
|
||||
errors.add :name, '名前に使用できない文字が含まれてゐます.'
|
||||
end
|
||||
end
|
||||
|
||||
def name_mustnt_start_with_nico
|
||||
if name.to_s.downcase.start_with?('nico:')
|
||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,37 +10,29 @@ class TagNameSanitisationRule < ApplicationRecord
|
||||
validate :source_pattern_must_be_regexp
|
||||
|
||||
class << self
|
||||
def sanitise(name)
|
||||
def sanitise(name) =
|
||||
rules.reduce(name.dup) { |name, (pattern, replacement)| name.gsub(pattern, replacement) }
|
||||
end
|
||||
|
||||
def apply!
|
||||
Language.find_each do |language|
|
||||
TagName.where(language:).find_each do |tn|
|
||||
TagName.find_each do |tn|
|
||||
name = sanitise(tn.name)
|
||||
next if name == tn.name
|
||||
|
||||
TagName.transaction do
|
||||
existing_tn = TagName.find_by(language:, name:)
|
||||
existing_tn = TagName.find_by(name:)
|
||||
if existing_tn
|
||||
unless existing_tn.primary_flg
|
||||
existing_tn = TagName.find_by!(language:,
|
||||
tag_id: existing_tn.tag_id,
|
||||
primary_flg: true)
|
||||
end
|
||||
existing_tn = existing_tn.canonical || existing_tn
|
||||
next if existing_tn.id == tn.id
|
||||
|
||||
existing_tag = existing_tn.tag
|
||||
source_tag = tn.tag
|
||||
existing_tag = Tag.find_by(tag_name_id: existing_tn.id)
|
||||
source_tag = Tag.find_by(tag_name_id: tn.id)
|
||||
|
||||
if existing_tag
|
||||
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
|
||||
elsif source_tag
|
||||
tn.update_columns(tag_id: nil, updated_at: Time.current)
|
||||
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
|
||||
existing_tn.update_columns(tag_id: source_tag.id, updated_at: Time.current)
|
||||
end
|
||||
tn.destroy!
|
||||
tn.discard!
|
||||
|
||||
next
|
||||
end
|
||||
@@ -50,7 +42,6 @@ class TagNameSanitisationRule < ApplicationRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -11,6 +11,5 @@ class TagVersion < ApplicationRecord
|
||||
meta: 'meta' }, validate: true
|
||||
|
||||
validates :name, presence: true
|
||||
validates :names, presence: true
|
||||
validates :category, presence: true
|
||||
end
|
||||
|
||||
@@ -6,12 +6,11 @@ module DeerjikistRepr
|
||||
|
||||
module_function
|
||||
|
||||
def base deerjikist, language_code: 'ja'
|
||||
deerjikist.as_json(BASE).merge(
|
||||
tag: deerjikist.tag && TagRepr.base(deerjikist.tag, language_code:))
|
||||
def base deerjikist
|
||||
deerjikist.as_json(BASE)
|
||||
end
|
||||
|
||||
def many deerjikists, language_code: 'ja'
|
||||
deerjikists.map { |d| base(d, language_code:) }
|
||||
def many deerjikists
|
||||
deerjikists.map { |d| base(d) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
module ExternalTagRepr
|
||||
module_function
|
||||
|
||||
def base tag
|
||||
{ 'id' => tag.id,
|
||||
'name' => "#{ tag.platform }:#{ tag.name }",
|
||||
'category' => 'nico',
|
||||
'post_count' => tag.post_count,
|
||||
'created_at' => tag.created_at,
|
||||
'updated_at' => tag.created_at,
|
||||
'deprecated_at' => nil,
|
||||
'aliases' => [],
|
||||
'parents' => [],
|
||||
'has_wiki' => false,
|
||||
'material_id' => nil,
|
||||
'has_deerjikists' => false }
|
||||
end
|
||||
|
||||
def inline(tag) = base(tag)
|
||||
end
|
||||
@@ -12,15 +12,14 @@ module MaterialRepr
|
||||
|
||||
module_function
|
||||
|
||||
def base material, host:, language_code: 'ja'
|
||||
def base material, host:
|
||||
material.as_json(BASE).merge(
|
||||
tag: material.tag && TagRepr.base(material.tag, language_code:),
|
||||
file: if material.file.attached?
|
||||
Rails.application.routes.url_helpers.rails_storage_proxy_url(
|
||||
material.file, host:)
|
||||
end,
|
||||
thumbnail: thumbnail_url(material, host:),
|
||||
thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:),
|
||||
thumbnail_fallback_text: thumbnail_fallback_text(material),
|
||||
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
|
||||
media_kind: media_kind(material),
|
||||
file_byte_size: material.file_byte_size,
|
||||
@@ -28,11 +27,11 @@ module MaterialRepr
|
||||
export_items: export_items(material))
|
||||
end
|
||||
|
||||
def many materials, host:, language_code: 'ja'
|
||||
materials.map { |m| base(m, host:, language_code:) }
|
||||
def many materials, host:
|
||||
materials.map { |m| base(m, host:) }
|
||||
end
|
||||
|
||||
def list material, host:, language_code: 'ja'
|
||||
def list material, host:
|
||||
{ id: material.id,
|
||||
version_no: material.version_no,
|
||||
url: material.url,
|
||||
@@ -41,9 +40,9 @@ module MaterialRepr
|
||||
source_path: material.source_path,
|
||||
source_file_id: material.source_file_id,
|
||||
normalized_source_key: material.normalized_source_key,
|
||||
tag: compact_tag(material.tag, language_code:),
|
||||
tag: compact_tag(material.tag),
|
||||
thumbnail: thumbnail_url(material, host:),
|
||||
thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:),
|
||||
thumbnail_fallback_text: thumbnail_fallback_text(material),
|
||||
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
|
||||
media_kind: media_kind(material),
|
||||
content_type: material.content_type,
|
||||
@@ -54,8 +53,8 @@ module MaterialRepr
|
||||
export_items: export_items(material) }
|
||||
end
|
||||
|
||||
def list_many materials, host:, language_code: 'ja'
|
||||
materials.map { |m| list(m, host:, language_code:) }
|
||||
def list_many materials, host:
|
||||
materials.map { |m| list(m, host:) }
|
||||
end
|
||||
|
||||
def export_paths material
|
||||
@@ -80,8 +79,8 @@ module MaterialRepr
|
||||
material.thumbnail, host:)
|
||||
end
|
||||
|
||||
def thumbnail_fallback_text material, language_code:
|
||||
material.tag&.name(language_code) || material.created_at&.strftime('%Y-%m-%d')
|
||||
def thumbnail_fallback_text material
|
||||
material.tag&.name || material.created_at&.strftime('%Y-%m-%d')
|
||||
end
|
||||
|
||||
def thumbnail_fallback_kind material
|
||||
@@ -99,11 +98,11 @@ module MaterialRepr
|
||||
'file_other'
|
||||
end
|
||||
|
||||
def compact_tag tag, language_code:
|
||||
def compact_tag tag
|
||||
return nil unless tag
|
||||
|
||||
{ id: tag.id,
|
||||
name: tag.name(language_code),
|
||||
name: tag.name,
|
||||
category: tag.category,
|
||||
deprecated_at: tag.deprecated_at }
|
||||
end
|
||||
|
||||
@@ -17,23 +17,23 @@ module PostRepr
|
||||
|
||||
module_function
|
||||
|
||||
def base post, current_user = nil, host: nil, language_code: 'ja'
|
||||
def base post, current_user = nil, host: nil
|
||||
json =
|
||||
if host.present?
|
||||
common(post, host:)
|
||||
else
|
||||
common(post)
|
||||
end
|
||||
json['tags'] = tag_json(post, language_code:)
|
||||
json['tags'] = tag_json(post)
|
||||
json['uploaded_user'] = post.uploaded_user && UserRepr.base(post.uploaded_user)
|
||||
json['viewed'] = current_user ? current_user.viewed?(post) : false
|
||||
json
|
||||
end
|
||||
|
||||
def detail post, current_user = nil, parent_posts: [], child_posts: [],
|
||||
sibling_posts: { }, related: [], host: nil, language_code: 'ja'
|
||||
sibling_posts: { }, related: [], host: nil
|
||||
if host.present?
|
||||
base(post, current_user, host:, language_code:).merge(
|
||||
base(post, current_user, host:).merge(
|
||||
'parent_posts' => cards(parent_posts, host:),
|
||||
'child_posts' => cards(child_posts, host:),
|
||||
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
|
||||
@@ -41,7 +41,7 @@ module PostRepr
|
||||
},
|
||||
'related' => cards(related, host:))
|
||||
else
|
||||
base(post, current_user, language_code:).merge(
|
||||
base(post, current_user).merge(
|
||||
'parent_posts' => cards(parent_posts),
|
||||
'child_posts' => cards(child_posts),
|
||||
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
|
||||
@@ -86,25 +86,16 @@ module PostRepr
|
||||
end)
|
||||
end
|
||||
|
||||
def tag_json post, language_code:
|
||||
internal_tags =
|
||||
def tag_json post
|
||||
post
|
||||
.post_tags
|
||||
.active_post_tags
|
||||
.reject { _1.tag.deprecated? }
|
||||
.sort_by { TagRepr.name_for(_1.tag, language_code) }
|
||||
.sort_by { _1.tag.name }
|
||||
.map { |post_tag|
|
||||
TagRepr.inline(post_tag.tag, language_code:).merge(
|
||||
TagRepr.inline(post_tag.tag).merge(
|
||||
'children' => [],
|
||||
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
|
||||
}
|
||||
|
||||
external_tags =
|
||||
post
|
||||
.external_tags
|
||||
.sort_by { _1.name }
|
||||
.map { ExternalTagRepr.base(_1).merge('children' => [], 'sections' => []) }
|
||||
|
||||
internal_tags + external_tags
|
||||
end
|
||||
|
||||
def thumbnail_url post, host: nil
|
||||
|
||||
@@ -3,23 +3,18 @@
|
||||
|
||||
module TagRepr
|
||||
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at, :deprecated_at],
|
||||
methods: [:has_wiki, :material_id, :has_deerjikists] }.freeze
|
||||
methods: [:name, :has_wiki, :material_id, :has_deerjikists] }.freeze
|
||||
|
||||
module_function
|
||||
|
||||
def base tag, language_code: 'ja'
|
||||
tag.as_json(BASE).merge(name: name_for(tag, language_code),
|
||||
aliases: tag.snapshot_aliases(language_code),
|
||||
parents: tag.parents.map { inline(_1, language_code:) })
|
||||
def base tag
|
||||
tag.as_json(BASE).merge(aliases: tag.snapshot_aliases,
|
||||
parents: tag.parents.map { _1.as_json(BASE) })
|
||||
end
|
||||
|
||||
def inline tag, language_code: 'ja'
|
||||
tag.as_json(BASE).merge(name: name_for(tag, language_code), aliases: [], parents: [])
|
||||
def inline tag
|
||||
tag.as_json(BASE).merge(aliases: [], parents: [])
|
||||
end
|
||||
|
||||
def many(tags, language_code: 'ja') = tags.map { |tag| base(tag, language_code:) }
|
||||
|
||||
def name_for tag, language_code
|
||||
tag.name(language_code) || (tag.system? ? tag.name('ja') : nil)
|
||||
end
|
||||
def many(tags) = tags.map { |t| base(t) }
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@ class MaterialVersionRecorder < VersionRecorder
|
||||
{ url: @record.url,
|
||||
parent: @record.parent,
|
||||
tag: @record.tag,
|
||||
tag_name: @record.tag&.name('ja'),
|
||||
tag_name: @record.tag&.name,
|
||||
tag_category: @record.tag&.category,
|
||||
source_kind: @record.source_kind,
|
||||
source_uri: @record.source_uri,
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
class NicoTagVersionRecorder < VersionRecorder
|
||||
def self.record! external_tag:, event_type:, created_by_user:
|
||||
new(external_tag:, event_type:, created_by_user:).record!
|
||||
def self.record! tag:, event_type:, created_by_user:
|
||||
new(tag:, event_type:, created_by_user:).record!
|
||||
end
|
||||
|
||||
def initialize external_tag:, event_type:, created_by_user:
|
||||
super(record: external_tag, event_type:, created_by_user:)
|
||||
def initialize tag:, event_type:, created_by_user:
|
||||
super(record: tag, event_type:, created_by_user:)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def version_class = NicoTagVersion
|
||||
def version_association = :nico_tag_versions
|
||||
def record_key = :external_tag
|
||||
def record_key = :tag
|
||||
|
||||
def snapshot_attributes
|
||||
{ name: "#{ @record.platform }:#{ @record.name }",
|
||||
linked_tags: @record.snapshot_linked_tag_names.join(' ') }
|
||||
{ name: @record.name, linked_tags: @record.snapshot_linked_tag_names.join(' ') }
|
||||
end
|
||||
|
||||
def tracks_version_no_on_record? = false
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class PostBulkCreator
|
||||
@host = host
|
||||
end
|
||||
|
||||
def run locale
|
||||
def run
|
||||
results = Array.new(@posts.length)
|
||||
mutex = Mutex.new
|
||||
next_index = 0
|
||||
@@ -27,7 +27,7 @@ class PostBulkCreator
|
||||
break if index >= @posts.length
|
||||
|
||||
attributes = @posts[index]
|
||||
results[index] = create_row(locale, actor, attributes, index)
|
||||
results[index] = create_row(actor, attributes, index)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error(
|
||||
"post_bulk_creator_worker_failure #{ { error: e.class.name,
|
||||
@@ -61,12 +61,12 @@ class PostBulkCreator
|
||||
|
||||
private
|
||||
|
||||
def create_row locale, actor, attributes, index
|
||||
def create_row actor, attributes, index
|
||||
preflight =
|
||||
PostCreatePreflight.new(
|
||||
attributes: attributes,
|
||||
thumbnail: thumbnail_for(index, attributes),
|
||||
host: @host).run(locale)
|
||||
host: @host).run
|
||||
if preflight[:existing_post_id].present?
|
||||
return {
|
||||
status: 'skipped',
|
||||
@@ -76,7 +76,7 @@ class PostBulkCreator
|
||||
|
||||
post = PostCreator.new(
|
||||
actor: actor,
|
||||
attributes: normalised_attributes(attributes, preflight, index)).create!(locale)
|
||||
attributes: normalised_attributes(attributes, preflight, index)).create!
|
||||
result = {
|
||||
status: 'created',
|
||||
post: { id: post.id } }
|
||||
|
||||
@@ -8,19 +8,20 @@ class PostCreatePlan
|
||||
@existing_tags_by_name = nil
|
||||
end
|
||||
|
||||
def build! locale
|
||||
direct_tag_specs, tag_sections = parse_direct_tag_specs(locale)
|
||||
default_tag_specs = build_default_tag_specs(locale, direct_tag_specs)
|
||||
def build!
|
||||
direct_tag_specs, tag_sections = parse_direct_tag_specs
|
||||
default_tag_specs = build_default_tag_specs(direct_tag_specs)
|
||||
snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs)
|
||||
preload_existing_tags_by_name!(locale, snapshot_tag_specs.map { _1[:name] })
|
||||
validate_new_tag_specs!(locale, snapshot_tag_specs)
|
||||
post_tag_specs = expand_parent_tag_specs(locale, snapshot_tag_specs)
|
||||
preload_existing_tags_by_name!(snapshot_tag_specs.map { _1[:name] })
|
||||
validate_new_tag_specs!(snapshot_tag_specs)
|
||||
post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
|
||||
video_ms = normalise_video_ms(snapshot_tag_specs)
|
||||
validate_video_sections!(video_ms, tag_sections)
|
||||
parent_post_ids = normalise_parent_post_ids
|
||||
validate_parent_post_ids!(parent_post_ids)
|
||||
|
||||
{ url: @attributes[:url],
|
||||
{
|
||||
url: @attributes[:url],
|
||||
title: @attributes[:title].to_s,
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence,
|
||||
original_created_from: @attributes[:original_created_from].presence,
|
||||
@@ -42,16 +43,15 @@ class PostCreatePlan
|
||||
|
||||
def tag_names = @attributes[:tags].to_s.split
|
||||
|
||||
def parse_direct_tag_specs locale
|
||||
def parse_direct_tag_specs
|
||||
tag_sections = { }
|
||||
direct_tag_specs = []
|
||||
|
||||
tag_names.each do |raw_name|
|
||||
tag_name, category, sections = parse_raw_tag_name(locale, raw_name)
|
||||
existing_tag = existing_tags_by_name(locale)[tag_name]
|
||||
if existing_tag&.deprecated?
|
||||
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name(locale.language_code)]
|
||||
end
|
||||
tag_name, category, sections = parse_raw_tag_name(raw_name)
|
||||
existing_tag = existing_tags_by_name[tag_name]
|
||||
raise Tag::NicoTagNormalisationError if existing_tag&.nico?
|
||||
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
|
||||
|
||||
direct_tag_specs << {
|
||||
name: tag_name,
|
||||
@@ -67,7 +67,7 @@ class PostCreatePlan
|
||||
[merge_tag_specs(direct_tag_specs), tag_sections]
|
||||
end
|
||||
|
||||
def parse_raw_tag_name locale, raw_name
|
||||
def parse_raw_tag_name raw_name
|
||||
name = raw_name.to_s
|
||||
prefix, category =
|
||||
Tag::CATEGORY_PREFIXES.find {
|
||||
@@ -76,7 +76,7 @@ class PostCreatePlan
|
||||
name = name.sub(/\A#{ prefix }/i, '')
|
||||
|
||||
sections = []
|
||||
while match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/)
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
@@ -89,17 +89,17 @@ class PostCreatePlan
|
||||
raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
[resolved_tag_name(locale, name), category&.to_sym, sections]
|
||||
[resolved_tag_name(name), category&.to_sym, sections]
|
||||
end
|
||||
|
||||
def build_default_tag_specs locale, direct_tag_specs
|
||||
def build_default_tag_specs direct_tag_specs
|
||||
default_tag_specs = []
|
||||
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
||||
default_tag_specs << {
|
||||
name: TAGME_TAG_NAME,
|
||||
category: :meta }
|
||||
end
|
||||
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
|
||||
if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
|
||||
default_tag_specs << {
|
||||
name: NO_DEERJIKIST_TAG_NAME,
|
||||
category: :meta }
|
||||
@@ -108,18 +108,17 @@ class PostCreatePlan
|
||||
default_tag_specs
|
||||
end
|
||||
|
||||
def validate_new_tag_specs! locale, specs
|
||||
def validate_new_tag_specs! specs
|
||||
Array(specs).each do |spec|
|
||||
next if existing_tags_by_name(locale).key?(spec[:name])
|
||||
next if existing_tags_by_name.key?(spec[:name])
|
||||
|
||||
validate_new_tag_spec!(locale, spec)
|
||||
validate_new_tag_spec!(spec)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_new_tag_spec! locale, spec
|
||||
tag_name = TagName.new(name: spec[:name], language_code: locale.language_code,
|
||||
script_code: locale.script_code, primary_flg: true)
|
||||
tag = Tag.new(category: spec[:category])
|
||||
def validate_new_tag_spec! spec
|
||||
tag_name = TagName.new(name: spec[:name])
|
||||
tag = Tag.new(category: spec[:category], tag_name:)
|
||||
return if tag_name.valid? && tag.valid?
|
||||
|
||||
post = Post.new
|
||||
@@ -134,22 +133,20 @@ class PostCreatePlan
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
|
||||
def expand_parent_tag_specs locale, snapshot_tag_specs
|
||||
existing_snapshot_tags = snapshot_tag_specs.filter_map do
|
||||
existing_tags_by_name(locale)[_1[:name]]
|
||||
end
|
||||
def expand_parent_tag_specs snapshot_tag_specs
|
||||
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
|
||||
expanded_parent_specs =
|
||||
Tag.expand_parent_tags(existing_snapshot_tags)
|
||||
.reject(&:deprecated?)
|
||||
.map { |tag|
|
||||
{ name: tag.name(locale.language_code),
|
||||
category: tag.category.to_sym }
|
||||
}
|
||||
{
|
||||
name: tag.name,
|
||||
category: tag.category.to_sym } }
|
||||
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
||||
end
|
||||
|
||||
def merge_tag_specs specs
|
||||
specs.each_with_object({ }) { |spec, merged|
|
||||
specs.each_with_object({ }) do |spec, merged|
|
||||
merged[spec[:name]] =
|
||||
if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general
|
||||
merged[spec[:name]]
|
||||
@@ -158,40 +155,36 @@ class PostCreatePlan
|
||||
name: spec[:name],
|
||||
category: spec[:category] }
|
||||
end
|
||||
}.values.sort_by { _1[:name] }
|
||||
end.values.sort_by { _1[:name] }
|
||||
end
|
||||
|
||||
def existing_tags_by_name locale
|
||||
def existing_tags_by_name
|
||||
@existing_tags_by_name ||= begin
|
||||
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
|
||||
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
|
||||
.includes(:tag)
|
||||
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
|
||||
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
|
||||
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
|
||||
end
|
||||
end
|
||||
|
||||
def preload_existing_tags_by_name! locale, names
|
||||
def preload_existing_tags_by_name! names
|
||||
wanted_names = Array(names).map { _1.to_s }.reject(&:blank?).uniq
|
||||
missing_names = wanted_names - existing_tags_by_name(locale).keys
|
||||
missing_names = wanted_names - existing_tags_by_name.keys
|
||||
return if missing_names.empty?
|
||||
|
||||
existing_tags_by_name(locale).merge!(
|
||||
TagName.where(language_code: locale.language_code,
|
||||
primary_flg: true,
|
||||
name: missing_names)
|
||||
.includes(:tag)
|
||||
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag })
|
||||
existing_tags_by_name.merge!(
|
||||
Tag.joins(:tag_name)
|
||||
.where(tag_names: { name: missing_names })
|
||||
.index_by(&:name))
|
||||
end
|
||||
|
||||
def canonical_tag_name_without_sections locale, raw_name
|
||||
name, = parse_raw_tag_name(locale, raw_name)
|
||||
def canonical_tag_name_without_sections raw_name
|
||||
name, = parse_raw_tag_name(raw_name)
|
||||
name
|
||||
end
|
||||
|
||||
def deerjikist_tag_spec? locale, spec
|
||||
def deerjikist_tag_spec? spec
|
||||
return true if spec[:category] == :deerjikist
|
||||
|
||||
existing_tags_by_name(locale)[spec[:name]]&.deerjikist?
|
||||
existing_tags_by_name[spec[:name]]&.deerjikist?
|
||||
end
|
||||
|
||||
def normalise_parent_post_ids
|
||||
@@ -269,12 +262,10 @@ class PostCreatePlan
|
||||
end
|
||||
end
|
||||
|
||||
def resolved_tag_name locale, name
|
||||
tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
||||
return name if !(tag_name) || tag_name.primary?
|
||||
def resolved_tag_name name
|
||||
tag_name = TagName.includes(:canonical).find_by(name:)
|
||||
return name if tag_name.nil?
|
||||
|
||||
TagName.find_by(language_code: locale.language_code,
|
||||
tag_id: tag_name.tag_id,
|
||||
primary_flg: true)&.name || name
|
||||
(tag_name.canonical || tag_name).name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,10 +15,9 @@ class PostCreatePreflight
|
||||
@host = host
|
||||
end
|
||||
|
||||
def run locale
|
||||
def run
|
||||
preview = PostImportPreviewer.new.preview_rows(
|
||||
rows: [preview_row],
|
||||
locale:,
|
||||
fetch_metadata: false).first
|
||||
if preview[:existing_post_id].present?
|
||||
return {
|
||||
@@ -54,7 +53,7 @@ class PostCreatePreflight
|
||||
original_created_from: preview[:attributes]['original_created_from'],
|
||||
original_created_before: preview[:attributes]['original_created_before'],
|
||||
duration: preview[:attributes]['duration'],
|
||||
video_ms: preview[:attributes]['video_ms'] }).build!(locale)
|
||||
video_ms: preview[:attributes]['video_ms'] }).build!
|
||||
|
||||
{
|
||||
url: plan[:url],
|
||||
|
||||
@@ -9,7 +9,7 @@ class PostCreator
|
||||
@field_warnings = { }
|
||||
end
|
||||
|
||||
def create! locale
|
||||
def create!
|
||||
thumbnail_attachment = prepare_thumbnail_attachment
|
||||
post = Post.new(title: @attributes[:title].presence,
|
||||
url: @attributes[:url],
|
||||
@@ -21,17 +21,14 @@ class PostCreator
|
||||
ApplicationRecord.transaction do
|
||||
post.save!
|
||||
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
|
||||
snapshot_tags = planned_snapshot_tags(locale)
|
||||
post_tags = planned_post_tags(locale)
|
||||
sections = planned_sections(locale)
|
||||
TagVersioning.record_tag_snapshots!(
|
||||
snapshot_tags,
|
||||
created_by_user: @actor,
|
||||
language_code: locale.language_code)
|
||||
post.video_ms = planned_video_ms(locale)
|
||||
snapshot_tags = planned_snapshot_tags
|
||||
post_tags = planned_post_tags
|
||||
sections = planned_sections
|
||||
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
|
||||
post.video_ms = planned_video_ms
|
||||
post.save!
|
||||
sync_post_tags!(post, post_tags, sections)
|
||||
sync_parent_posts!(post, planned_parent_post_ids(locale))
|
||||
sync_parent_posts!(post, planned_parent_post_ids)
|
||||
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
|
||||
end
|
||||
post
|
||||
@@ -48,73 +45,69 @@ class PostCreator
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence)
|
||||
end
|
||||
|
||||
def planned_snapshot_tags(locale) = planned_create_attributes(locale)[:snapshot_tags]
|
||||
def planned_snapshot_tags = planned_create_attributes[:snapshot_tags]
|
||||
|
||||
def planned_post_tags(locale) = planned_create_attributes(locale)[:post_tags]
|
||||
def planned_post_tags = planned_create_attributes[:post_tags]
|
||||
|
||||
def planned_sections(locale) = planned_create_attributes(locale)[:tag_sections]
|
||||
def planned_sections = planned_create_attributes[:tag_sections]
|
||||
|
||||
def planned_parent_post_ids locale
|
||||
planned_create_attributes(locale)[:normalised_parent_post_ids]
|
||||
def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]
|
||||
|
||||
def planned_video_ms
|
||||
planned_create_attributes[:video_ms]
|
||||
end
|
||||
|
||||
def planned_video_ms locale
|
||||
planned_create_attributes(locale)[:video_ms]
|
||||
end
|
||||
|
||||
def planned_create_attributes locale
|
||||
def planned_create_attributes
|
||||
@planned_create_attributes ||= begin
|
||||
if @attributes.key?(:snapshot_tag_specs)
|
||||
snapshot_tags = materialise_tags(locale, @attributes[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(locale, @attributes[:post_tag_specs] || [])
|
||||
snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
|
||||
{
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
@attributes[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags,
|
||||
locale),
|
||||
post_tags),
|
||||
normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
|
||||
video_ms: @attributes[:video_ms] }
|
||||
else
|
||||
build_materialised_plan(locale)
|
||||
build_materialised_plan
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_materialised_plan locale
|
||||
plan = PostCreatePlan.new(attributes: @attributes).build!(locale)
|
||||
snapshot_tags = materialise_tags(locale, plan[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(locale, plan[:post_tag_specs] || [])
|
||||
def build_materialised_plan
|
||||
plan = PostCreatePlan.new(attributes: @attributes).build!
|
||||
snapshot_tags = materialise_tags(plan[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(plan[:post_tag_specs] || [])
|
||||
{
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
plan[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags,
|
||||
locale),
|
||||
post_tags),
|
||||
normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
|
||||
video_ms: plan[:video_ms] }
|
||||
end
|
||||
|
||||
def materialise_tags locale, specs
|
||||
def materialise_tags specs
|
||||
Array(specs).each_with_object({ }) do |spec, tags|
|
||||
name = spec[:name] || spec['name']
|
||||
category = spec[:category] || spec['category']
|
||||
next if name.blank? || category.blank?
|
||||
|
||||
tag = Tag.find_or_create_by_tag_name!(locale, name, category:)
|
||||
tag = Tag.find_or_create_by_tag_name!(name, category:)
|
||||
tag.update!(category:) if tag.category.to_sym != category.to_sym
|
||||
tags[name] ||= tag
|
||||
end.values
|
||||
end
|
||||
|
||||
def materialise_sections sections_by_name, snapshot_tags, post_tags, locale = Locale.nipponese
|
||||
tags_by_name = post_tags.index_by { _1.name(locale.language_code) }
|
||||
def materialise_sections sections_by_name, snapshot_tags, post_tags
|
||||
tags_by_name = post_tags.index_by(&:name)
|
||||
snapshot_tags.each do |tag|
|
||||
tags_by_name[tag.name(locale.language_code)] ||= tag
|
||||
tags_by_name[tag.name] ||= tag
|
||||
end
|
||||
|
||||
sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
|
||||
@@ -128,11 +121,9 @@ class PostCreator
|
||||
def sync_post_tags! post, desired_tags, sections
|
||||
desired_ids = desired_tags.map(&:id).to_set
|
||||
current_ids = post.tags.pluck(:id).to_set
|
||||
|
||||
Tag.where(id: desired_ids - current_ids).find_each do |tag|
|
||||
PostTag.create_or_find_by!(post:, tag:, created_user: @actor)
|
||||
end
|
||||
|
||||
PostTagSection.where(post_id: post.id).destroy_all
|
||||
sections.each do |tag_id, ranges|
|
||||
ranges.each do |begin_ms, end_ms|
|
||||
@@ -142,9 +133,10 @@ class PostCreator
|
||||
end_ms:)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def sync_parent_posts! post, ids
|
||||
|
||||
@@ -16,7 +16,7 @@ class PostImportPreviewer
|
||||
THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze
|
||||
METADATA_FETCH_WARNING = '自動取得に失敗しました.'.freeze
|
||||
|
||||
def preview_rows rows:, locale: Locale.nipponese, fetch_metadata: true, metadata_cache: { }
|
||||
def preview_rows rows:, fetch_metadata: true, metadata_cache: { }
|
||||
prepared_rows = rows.map { prepare_row(_1) }
|
||||
url_counts = prepared_rows.filter_map { _1[:normal_url] }.tally
|
||||
existing_posts =
|
||||
@@ -30,14 +30,13 @@ class PostImportPreviewer
|
||||
url_counts)
|
||||
known_tags =
|
||||
preload_known_tags(
|
||||
locale,
|
||||
prepared_rows,
|
||||
fetch_metadata,
|
||||
metadata_cache,
|
||||
existing_posts,
|
||||
url_counts)
|
||||
prepared_rows.map { |row|
|
||||
preview_row(locale, row,
|
||||
preview_row(row,
|
||||
fetch_metadata:,
|
||||
metadata_cache:,
|
||||
existing_posts:,
|
||||
@@ -60,7 +59,7 @@ class PostImportPreviewer
|
||||
source.merge(url_text: url, normal_url:, url_error: validate_url_safety(normal_url))
|
||||
end
|
||||
|
||||
def preview_row locale, row,
|
||||
def preview_row row,
|
||||
fetch_metadata:,
|
||||
metadata_cache:,
|
||||
existing_posts:,
|
||||
@@ -125,8 +124,7 @@ class PostImportPreviewer
|
||||
|
||||
attributes['url'] = url
|
||||
validate_basic_data(attributes, validation_errors)
|
||||
validate_preview_tags(locale,
|
||||
merged_tags(tag_sources, provenance['tags']),
|
||||
validate_preview_tags(merged_tags(tag_sources, provenance['tags']),
|
||||
validation_errors,
|
||||
known_tags)
|
||||
validate_parents(attributes['parent_post_ids'], validation_errors, existing_parent_ids)
|
||||
@@ -348,7 +346,7 @@ class PostImportPreviewer
|
||||
{ data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } }
|
||||
end
|
||||
|
||||
def preload_known_tags locale, prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts
|
||||
def preload_known_tags prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts
|
||||
names = prepared_rows.flat_map { |row|
|
||||
attributes = initial_attributes(row)
|
||||
provenance = initial_provenance(row)
|
||||
@@ -360,13 +358,15 @@ class PostImportPreviewer
|
||||
metadata = metadata_for(row[:normal_url], metadata_cache)
|
||||
apply_metadata!(attributes, provenance, tag_sources, metadata[:data])
|
||||
end
|
||||
preview_tag_names(locale, merged_tags(tag_sources, provenance['tags']))
|
||||
preview_tag_names(merged_tags(tag_sources, provenance['tags']))
|
||||
}.compact.uniq
|
||||
return { } if names.empty?
|
||||
|
||||
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
|
||||
.includes(:tag)
|
||||
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
|
||||
Tag.joins(:tag_name)
|
||||
.where(tag_names: { name: names })
|
||||
.includes(:tag_name)
|
||||
.to_a
|
||||
.index_by(&:name)
|
||||
end
|
||||
|
||||
def preload_parent_ids prepared_rows
|
||||
@@ -437,21 +437,19 @@ class PostImportPreviewer
|
||||
sources['automatic'].to_s
|
||||
end
|
||||
|
||||
def preview_tag_names locale, raw
|
||||
def preview_tag_names raw
|
||||
names = raw.to_s.split
|
||||
return [] if names.empty?
|
||||
if names.any? { _1.downcase.start_with?('nico:') }
|
||||
return []
|
||||
end
|
||||
|
||||
names.map { |name|
|
||||
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
|
||||
}
|
||||
names.map { |name| TagName.canonicalise(name.sub(/\[.*\]\z/, '')).first }
|
||||
rescue Tag::SectionLiteralParseError
|
||||
[]
|
||||
end
|
||||
|
||||
def validate_preview_tags locale, raw, errors, known_tags
|
||||
def validate_preview_tags raw, errors, known_tags
|
||||
names = raw.to_s.split
|
||||
return if names.empty?
|
||||
if names.any? { _1.downcase.start_with?('nico:') }
|
||||
@@ -459,11 +457,9 @@ class PostImportPreviewer
|
||||
return
|
||||
end
|
||||
|
||||
parsed = names.map { |name|
|
||||
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
|
||||
}
|
||||
parsed = names.map { |name| TagName.canonicalise(name.sub(/\[.*\]\z/, '')).first }
|
||||
existing = parsed.filter_map { known_tags[_1] }
|
||||
deprecated = existing.select(&:deprecated?).map { _1.name(locale.language_code) }
|
||||
deprecated = existing.select(&:deprecated?).map(&:name)
|
||||
errors[:tags] = ["廃止済みタグがあります: #{ deprecated.join(' ') }"] if deprecated.present?
|
||||
rescue Tag::SectionLiteralParseError
|
||||
errors[:tags] = ['タグ区間の記法が不正です.']
|
||||
|
||||
@@ -47,9 +47,9 @@ class PostMetadataFetcher
|
||||
|
||||
existing_tags =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: 'ja', primary_flg: true, name: names })
|
||||
.index_by { _1.name('ja') }
|
||||
.joins(:tag_name)
|
||||
.where(tag_names: { name: names })
|
||||
.index_by(&:name)
|
||||
|
||||
names.map { |name|
|
||||
tag = existing_tags[name]
|
||||
|
||||
@@ -25,7 +25,6 @@ class PostVersionRecorder < VersionRecorder
|
||||
thumbnail_base: @record.thumbnail_base,
|
||||
video_ms: @record.video_ms,
|
||||
tags: @record.snapshot_tag_names.join(' '),
|
||||
tags_json: @record.snapshot_tags_json,
|
||||
parent_post_ids: @record.snapshot_parent_post_ids.join(' '),
|
||||
original_created_from: @record.original_created_from,
|
||||
original_created_before: @record.original_created_before }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class TagVersionRecorder < VersionRecorder
|
||||
def self.record! tag:, event_type:, created_by_user:, **_options
|
||||
def self.record! tag:, event_type:, created_by_user:
|
||||
new(tag:, event_type:, created_by_user:).record!
|
||||
end
|
||||
|
||||
@@ -14,25 +14,10 @@ class TagVersionRecorder < VersionRecorder
|
||||
def record_key = :tag
|
||||
|
||||
def snapshot_attributes
|
||||
names = snapshot_names
|
||||
japanese_names = names.select { _1['language_code'] == 'ja' }
|
||||
compatibility_names = japanese_names.presence || names
|
||||
|
||||
{ name: compatibility_names.find { _1['primary_flg'] }['name'],
|
||||
{ name: @record.name,
|
||||
category: @record.category,
|
||||
deprecated_at: @record.deprecated_at,
|
||||
aliases: compatibility_names.reject { _1['primary_flg'] }.map { _1['name'] }.join(' '),
|
||||
names:,
|
||||
aliases: @record.snapshot_aliases.join(' '),
|
||||
parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') }
|
||||
end
|
||||
|
||||
def snapshot_names
|
||||
@record.tag_names.order(:language_code, primary_flg: :desc).order(:name).map { |tag_name|
|
||||
{ 'language_code' => tag_name.language_code,
|
||||
'name' => tag_name.name,
|
||||
'script_code' => tag_name.script_code,
|
||||
'primary_flg' => tag_name.primary_flg,
|
||||
'auto_generated' => tag_name.auto_generated }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
class TagVersioning
|
||||
def self.record! tag, event_type:, created_by_user:, language_code: 'ja'
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
|
||||
def self.record! tag, event_type:, created_by_user:
|
||||
if tag.nico?
|
||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
else
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
end
|
||||
end
|
||||
|
||||
def self.ensure_snapshot! tag, created_by_user:, language_code: 'ja'
|
||||
def self.ensure_snapshot! tag, created_by_user:
|
||||
if tag.nico?
|
||||
return if tag.nico_tag_versions.exists?
|
||||
|
||||
NicoTagVersionRecorder.record!(tag:, event_type: :create, created_by_user:)
|
||||
else
|
||||
return if tag.tag_versions.exists?
|
||||
|
||||
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:, language_code:)
|
||||
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:)
|
||||
end
|
||||
end
|
||||
|
||||
def self.record_tag_snapshot! tag, created_by_user:, language_code: 'ja'
|
||||
event_type = tag.tag_versions.exists? ? :update : :create
|
||||
record!(tag, event_type:, created_by_user:, language_code:)
|
||||
def self.record_tag_snapshot! tag, created_by_user:
|
||||
event_type =
|
||||
if tag.nico?
|
||||
tag.nico_tag_versions.exists? ? :update : :create
|
||||
else
|
||||
tag.tag_versions.exists? ? :update : :create
|
||||
end
|
||||
|
||||
def self.record_tag_snapshots! tags, created_by_user:, language_code: 'ja'
|
||||
record!(tag, event_type:, created_by_user:)
|
||||
end
|
||||
|
||||
def self.record_tag_snapshots! tags, created_by_user:
|
||||
tags.each do |tag|
|
||||
record_tag_snapshot!(tag, created_by_user:, language_code:)
|
||||
record_tag_snapshot!(tag, created_by_user:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ class TheatrePostSelector
|
||||
def weighted_candidates
|
||||
@weighted_candidates ||= begin
|
||||
penalties = tag_penalties
|
||||
posts = eligible_posts.includes(tags: :tag_names).to_a
|
||||
posts = eligible_posts.includes(tags: :tag_name).to_a
|
||||
|
||||
posts.map do |post|
|
||||
post_tags = post.tags.to_a
|
||||
@@ -82,7 +82,7 @@ class TheatrePostSelector
|
||||
def tag_penalty_json
|
||||
return [] if tag_penalties.empty?
|
||||
|
||||
tags = Tag.where(id: tag_penalties.keys).includes(:tag_names).index_by(&:id)
|
||||
tags = Tag.where(id: tag_penalties.keys).includes(:tag_name).index_by(&:id)
|
||||
|
||||
tag_penalties
|
||||
.map { |tag_id, penalty|
|
||||
@@ -113,7 +113,7 @@ class TheatrePostSelector
|
||||
|
||||
def light_tag_json tag
|
||||
{ id: tag.id,
|
||||
name: tag.name('ja'),
|
||||
name: tag.name,
|
||||
category: tag.category }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,14 +47,10 @@ class VersionRecorder
|
||||
end
|
||||
|
||||
def update_record_version_no! version_no
|
||||
return unless tracks_version_no_on_record?
|
||||
|
||||
@record.update_columns(version_no:)
|
||||
@record.version_no = version_no
|
||||
end
|
||||
|
||||
def tracks_version_no_on_record? = true
|
||||
|
||||
def validate_version_sequence! latest
|
||||
if !(latest) && @event_type != 'create'
|
||||
raise "#{ version_class.name } first event must be create"
|
||||
@@ -64,7 +60,7 @@ class VersionRecorder
|
||||
raise "#{ version_class.name } create event already exists"
|
||||
end
|
||||
|
||||
return if !(latest) || !(tracks_version_no_on_record?)
|
||||
return unless latest
|
||||
|
||||
if @record.version_no != latest.version_no
|
||||
raise ("#{ record_class.name }##{ @record.id } version_no is #{ @record.version_no }, " +
|
||||
|
||||
@@ -103,7 +103,7 @@ module Youtube
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
to_add = desired_tag_ids - current_tag_ids
|
||||
@@ -117,8 +117,8 @@ module Youtube
|
||||
end
|
||||
end
|
||||
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each do |pt|
|
||||
pt.destroy!
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||
pt.discard_by!(nil)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Rails.application.routes.draw do
|
||||
resources :nico_tags, path: 'tags/nico', only: [:index, :show, :update]
|
||||
resources :nico_tags, path: 'tags/nico', only: [:index, :update]
|
||||
|
||||
scope 'tags/:parent_id/children', controller: :tag_children do
|
||||
post ':child_id', action: :create
|
||||
@@ -55,6 +55,7 @@ Rails.application.routes.draw do
|
||||
get :metadata
|
||||
post :bulk
|
||||
get :random
|
||||
get :changes
|
||||
get :versions, to: 'post_versions#index'
|
||||
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,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
|
||||
@@ -1,54 +0,0 @@
|
||||
class CreateExternalTags < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
create_table :external_tags do |t|
|
||||
t.string :platform, limit: 16, null: false
|
||||
t.string :name, limit: 255, null: false
|
||||
t.integer :post_count, null: false, default: 0
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [:platform, :name], unique: true
|
||||
end
|
||||
|
||||
execute <<~SQL
|
||||
INSERT INTO
|
||||
external_tags(id, platform, name, post_count, created_at)
|
||||
SELECT
|
||||
t.id
|
||||
, 'nico' AS platform
|
||||
, SUBSTR(tn.name, 6) AS name
|
||||
, t.post_count
|
||||
, t.created_at
|
||||
FROM
|
||||
tags t
|
||||
INNER JOIN
|
||||
tag_names tn
|
||||
ON
|
||||
tn.id = t.tag_name_id
|
||||
AND t.category = 'nico'
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
INSERT INTO
|
||||
external_tags(id, platform, name, post_count, created_at)
|
||||
SELECT
|
||||
ntv.tag_id
|
||||
, 'nico' AS platform
|
||||
, SUBSTR(ntv.name, 6) AS name
|
||||
, 0 AS post_count
|
||||
, ntv.created_at
|
||||
FROM
|
||||
nico_tag_versions ntv
|
||||
LEFT JOIN
|
||||
external_tags et
|
||||
ON
|
||||
et.id = ntv.tag_id
|
||||
WHERE
|
||||
ntv.version_no = 1
|
||||
AND et.id IS NULL
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :external_tags
|
||||
end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
class CreatePostExternalTags < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
create_table :post_external_tags, primary_key: [:post_id, :external_tag_id] do |t|
|
||||
t.references :post, null: false, index: false, foreign_key: true
|
||||
t.references :external_tag, null: false, foreign_key: true
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
|
||||
execute <<~SQL
|
||||
INSERT INTO
|
||||
post_external_tags(post_id, external_tag_id, created_at)
|
||||
SELECT
|
||||
pt.post_id
|
||||
, pt.tag_id AS external_tag_id
|
||||
, pt.created_at
|
||||
FROM
|
||||
post_tags pt
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
pt.tag_id = t.id
|
||||
AND t.category = 'nico'
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :post_external_tags
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
class ChangeForeignKeyOnNicoTagRelations < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
remove_foreign_key :nico_tag_relations, :tags, column: :nico_tag_id
|
||||
add_foreign_key :nico_tag_relations, :external_tags, column: :nico_tag_id
|
||||
end
|
||||
end
|
||||
@@ -1,199 +0,0 @@
|
||||
class MigrateExternalTags < ActiveRecord::Migration[8.0]
|
||||
class MigrationPostVersion < ActiveRecord::Base
|
||||
self.table_name = 'post_versions'
|
||||
end
|
||||
|
||||
def up
|
||||
x = connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
post_tag_sections pts
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.id = pts.tag_id
|
||||
WHERE
|
||||
t.category = 'nico'
|
||||
SQL
|
||||
if x > 0
|
||||
raise "post_tag_sections に #{ x } 件のチンカスがあります!"
|
||||
end
|
||||
|
||||
x = connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
materials m
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.id = m.tag_id
|
||||
WHERE
|
||||
t.category = 'nico'
|
||||
SQL
|
||||
if x > 0
|
||||
raise "materials に #{ x } 件のチンカスがあります!"
|
||||
end
|
||||
|
||||
x = connection.select_value(<<~SQL)
|
||||
SELECT
|
||||
COUNT(0)
|
||||
FROM
|
||||
tag_implications ti
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.id = ti.tag_id
|
||||
OR t.id = ti.parent_tag_id
|
||||
WHERE
|
||||
t.category = 'nico'
|
||||
SQL
|
||||
if x > 0
|
||||
raise "tag_implications に #{ x } 件のチンカスがあります!"
|
||||
end
|
||||
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
ts
|
||||
FROM
|
||||
tag_similarities ts
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.category = 'nico'
|
||||
AND (t.id = ts.tag_id
|
||||
OR t.id = ts.target_tag_id)
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
tset
|
||||
FROM
|
||||
theatre_skip_event_tags tset
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.category = 'nico'
|
||||
AND t.id = tset.tag_id
|
||||
SQL
|
||||
|
||||
remove_check_constraint :post_versions, name: 'chk_post_versions_tags_json_schema'
|
||||
|
||||
say_with_time 'Migrate post_versions.tags_json' do
|
||||
count = 0
|
||||
|
||||
MigrationPostVersion.find_each(batch_size: 500) do |version|
|
||||
tags = version.tags_json.map do |tag|
|
||||
if tag.fetch('category') == 'nico'
|
||||
{ 'external_tag_id' => tag.fetch('id') }
|
||||
else
|
||||
{ 'tag_id' => tag.fetch('id'),
|
||||
'version_no' => tag.fetch('version_no'),
|
||||
'name' => tag.fetch('name'),
|
||||
'category' => tag.fetch('category'),
|
||||
'sections' => tag.fetch('sections') }
|
||||
end
|
||||
end
|
||||
|
||||
version.update_columns(tags_json: tags)
|
||||
count += 1
|
||||
end
|
||||
|
||||
count
|
||||
end
|
||||
|
||||
add_tags_json_constraint!
|
||||
|
||||
tag_name_ids = connection.select_values(<<~SQL)
|
||||
SELECT
|
||||
tag_name_id
|
||||
FROM
|
||||
tags
|
||||
WHERE
|
||||
category = 'nico'
|
||||
SQL
|
||||
|
||||
connection.transaction do
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
pt
|
||||
FROM
|
||||
post_tags pt
|
||||
INNER JOIN
|
||||
tags t
|
||||
ON
|
||||
t.category = 'nico'
|
||||
AND t.id = pt.tag_id
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
FROM
|
||||
tags
|
||||
WHERE
|
||||
category = 'nico'
|
||||
SQL
|
||||
|
||||
unless tag_name_ids.empty?
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
FROM
|
||||
tag_names
|
||||
WHERE
|
||||
id IN (#{ tag_name_ids.join(', ') })
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_tags_json_constraint!
|
||||
schema = { type: 'array',
|
||||
items: { oneOf: [internal_tag_schema, external_tag_schema] } }
|
||||
|
||||
quoted_schema = connection.quote(JSON.generate(schema))
|
||||
|
||||
add_check_constraint :post_versions,
|
||||
"JSON_SCHEMA_VALID(#{ quoted_schema }, tags_json)",
|
||||
name: 'chk_post_versions_tags_json_schema'
|
||||
end
|
||||
|
||||
def internal_tag_schema
|
||||
{ type: 'object',
|
||||
properties: { tag_id: { type: 'integer', minimum: 1 },
|
||||
version_no: { type: 'integer', minimum: 1 },
|
||||
name: { type: 'string', minLength: 1 },
|
||||
category: { type: 'string',
|
||||
enum: ['deerjikist',
|
||||
'meme',
|
||||
'character',
|
||||
'general',
|
||||
'material',
|
||||
'meta'] },
|
||||
sections: sections_schema },
|
||||
required: ['tag_id', 'version_no', 'sections'],
|
||||
additionalProperties: false }
|
||||
end
|
||||
|
||||
def external_tag_schema
|
||||
{ type: 'object',
|
||||
properties: { external_tag_id: { type: 'integer', minimum: 1 } },
|
||||
required: ['external_tag_id'],
|
||||
additionalProperties: false }
|
||||
end
|
||||
|
||||
def sections_schema
|
||||
{ type: 'array',
|
||||
items: { type: 'object',
|
||||
properties: { begin_ms: { type: 'integer', minimum: 0 },
|
||||
end_ms: { type: ['integer', 'null'], minimum: 0 } },
|
||||
required: ['begin_ms', 'end_ms'],
|
||||
additionalProperties: false } }
|
||||
end
|
||||
end
|
||||
@@ -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,38 +0,0 @@
|
||||
class AddNamesToTagVersions < ActiveRecord::Migration[8.0]
|
||||
class TagVersion < ActiveRecord::Base
|
||||
self.table_name = 'tag_versions'
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :tag_versions, :names, :json
|
||||
TagVersion.reset_column_information
|
||||
|
||||
say_with_time 'Backfilling multilingual tag version names' do
|
||||
TagVersion.find_each do |version|
|
||||
names = [{
|
||||
language_code: 'ja',
|
||||
name: version.name,
|
||||
script_code: 'Jpan',
|
||||
primary_flg: true,
|
||||
auto_generated: false }]
|
||||
names.concat(version.aliases.to_s.split.filter_map { |name|
|
||||
next if name.blank?
|
||||
|
||||
{
|
||||
language_code: 'ja',
|
||||
name:,
|
||||
script_code: 'Jpan',
|
||||
primary_flg: false,
|
||||
auto_generated: false }
|
||||
})
|
||||
version.update_columns(names: names)
|
||||
end
|
||||
end
|
||||
|
||||
change_column_null :tag_versions, :names, false
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :tag_versions, :names
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
class AddAutoGeneratedToTagNames < ActiveRecord::Migration[8.0]
|
||||
class TagVersion < ActiveRecord::Base
|
||||
self.table_name = 'tag_versions'
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :tag_names, :auto_generated, :boolean, null: false, default: false
|
||||
TagVersion.reset_column_information
|
||||
|
||||
TagVersion.find_each do |version|
|
||||
names = Array(version.names).map { |name| name.merge('auto_generated' => false) }
|
||||
version.update_columns(names: names)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :tag_names, :auto_generated
|
||||
end
|
||||
end
|
||||
生成ファイル
+38
-77
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -48,14 +48,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.index ["tag_id"], name: "index_deerjikists_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "external_tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "platform", limit: 16, null: false
|
||||
t.string "name", null: false
|
||||
t.integer "post_count", default: 0, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["platform", "name"], name: "index_external_tags_on_platform_and_name", unique: true
|
||||
end
|
||||
|
||||
create_table "gekanator_ai_runs", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "model", null: false
|
||||
t.integer "input_tokens", default: 0, null: false
|
||||
@@ -138,24 +130,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true
|
||||
end
|
||||
|
||||
create_table "languages", primary_key: "code", id: { type: :string, limit: 16 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.datetime "deprecated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["deprecated_at"], name: "index_languages_on_deprecated_at"
|
||||
end
|
||||
|
||||
create_table "locales", primary_key: "code", id: { type: :string, limit: 32 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "language_code", limit: 16, null: false
|
||||
t.string "script_code", limit: 4, null: false
|
||||
t.string "name", null: false
|
||||
t.datetime "deprecated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["deprecated_at"], name: "index_locales_on_deprecated_at"
|
||||
t.index ["language_code"], name: "index_locales_on_language_code"
|
||||
t.index ["script_code"], name: "fk_rails_0b74ce96a8"
|
||||
end
|
||||
|
||||
create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "material_id", null: false
|
||||
t.string "profile", default: "legacy_drive", null: false
|
||||
@@ -307,13 +281,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive"
|
||||
end
|
||||
|
||||
create_table "post_external_tags", primary_key: ["post_id", "external_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "post_id", null: false
|
||||
t.bigint "external_tag_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["external_tag_id"], name: "index_post_external_tags_on_external_tag_id"
|
||||
end
|
||||
|
||||
create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "post_id", null: false
|
||||
t.bigint "parent_post_id", null: false
|
||||
@@ -344,12 +311,23 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.check_constraint "`begin_ms` >= 0", name: "chk_post_tag_sections_begin_ms_natural"
|
||||
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 "tag_id", null: false
|
||||
t.bigint "created_user_id"
|
||||
t.bigint "deleted_user_id"
|
||||
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 ["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"
|
||||
end
|
||||
|
||||
@@ -371,7 +349,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.string "url", limit: 768, null: false
|
||||
t.string "thumbnail_base", limit: 2000
|
||||
t.text "tags", null: false
|
||||
t.json "tags_json", null: false
|
||||
t.text "parent_post_ids", null: false
|
||||
t.datetime "original_created_from"
|
||||
t.datetime "original_created_before"
|
||||
@@ -385,7 +362,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive"
|
||||
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "post_versions_event_type_valid"
|
||||
t.check_constraint "`version_no` > 0", name: "post_versions_version_no_positive"
|
||||
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"tag_id\",\"version_no\",\"sections\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"external_tag_id\":{\"type\":\"integer\",\"minimum\":1}},\"required\":[\"external_tag_id\"],\"additionalProperties\":false}]}}',`tags_json`)", name: "chk_post_versions_tags_json_schema"
|
||||
end
|
||||
|
||||
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
@@ -406,13 +382,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
|
||||
end
|
||||
|
||||
create_table "scripts", primary_key: "code", id: { type: :string, limit: 4 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.datetime "deprecated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["deprecated_at"], name: "index_scripts_on_deprecated_at"
|
||||
end
|
||||
|
||||
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -424,6 +393,19 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
||||
end
|
||||
|
||||
create_table "wiki_assets", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "wiki_page_id", null: false
|
||||
t.integer "no", null: false
|
||||
t.string "alt_text"
|
||||
t.binary "sha256", limit: 32, null: false
|
||||
t.bigint "created_by_user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["created_by_user_id"], name: "index_wiki_assets_on_created_by_user_id"
|
||||
t.index ["wiki_page_id", "no"], name: "index_wiki_assets_on_wiki_page_id_and_no", unique: true
|
||||
t.index ["wiki_page_id", "sha256"], name: "index_wiki_assets_on_wiki_page_id_and_sha256", unique: true
|
||||
end
|
||||
|
||||
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "tag_id", null: false
|
||||
t.bigint "parent_tag_id", null: false
|
||||
@@ -445,21 +427,14 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
end
|
||||
|
||||
create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "tag_id"
|
||||
t.string "language_code", limit: 16, null: false
|
||||
t.string "name", null: false
|
||||
t.string "script_code", limit: 4, null: false
|
||||
t.boolean "primary_flg", null: false
|
||||
t.bigint "canonical_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.virtual "primary_tag_id", type: :bigint, as: "(case when `primary_flg` then `tag_id` else NULL end)"
|
||||
t.boolean "auto_generated", default: false, null: false
|
||||
t.datetime "discarded_at"
|
||||
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
|
||||
t.index ["language_code", "name"], name: "index_tag_names_on_language_code_and_name", unique: true
|
||||
t.index ["primary_tag_id", "language_code"], name: "index_tag_names_on_primary_tag_id_and_language_code", unique: true
|
||||
t.index ["script_code"], name: "fk_rails_dd783b3d1c"
|
||||
t.index ["tag_id"], name: "index_tag_names_on_tag_id"
|
||||
t.index ["discarded_at"], name: "index_tag_names_on_discarded_at"
|
||||
t.index ["name"], name: "index_tag_names_on_name", unique: true
|
||||
end
|
||||
|
||||
create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
@@ -481,7 +456,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.datetime "deprecated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "created_by_user_id"
|
||||
t.json "names", null: false
|
||||
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
||||
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
|
||||
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
|
||||
@@ -496,8 +470,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "post_count", default: 0, null: false
|
||||
t.datetime "discarded_at"
|
||||
t.integer "version_no", null: false
|
||||
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.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"
|
||||
@@ -638,19 +614,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
t.index ["banned_at"], name: "index_users_on_banned_at"
|
||||
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|
|
||||
t.string "sha256", limit: 64, null: false
|
||||
t.text "body", null: false
|
||||
@@ -736,8 +699,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
add_foreign_key "gekanator_question_suggestions", "users"
|
||||
add_foreign_key "gekanator_questions", "gekanator_question_suggestions"
|
||||
add_foreign_key "gekanator_questions", "users", column: "created_by_id"
|
||||
add_foreign_key "locales", "languages", column: "language_code", primary_key: "code"
|
||||
add_foreign_key "locales", "scripts", column: "script_code", primary_key: "code"
|
||||
add_foreign_key "material_export_items", "materials"
|
||||
add_foreign_key "material_export_items", "users", column: "created_by_user_id"
|
||||
add_foreign_key "material_import_blocks", "users", column: "created_by_user_id"
|
||||
@@ -746,37 +707,37 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
|
||||
add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id"
|
||||
add_foreign_key "material_versions", "materials"
|
||||
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: "updated_by_user_id"
|
||||
add_foreign_key "materials", "materials", column: "parent_id"
|
||||
add_foreign_key "materials", "tags"
|
||||
add_foreign_key "materials", "users", column: "created_by_user_id"
|
||||
add_foreign_key "materials", "users", column: "updated_by_user_id"
|
||||
add_foreign_key "nico_tag_relations", "external_tags", column: "nico_tag_id"
|
||||
add_foreign_key "nico_tag_relations", "tags"
|
||||
add_foreign_key "nico_tag_relations", "tags", 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 "post_external_tags", "external_tags"
|
||||
add_foreign_key "post_external_tags", "posts"
|
||||
add_foreign_key "post_implications", "posts"
|
||||
add_foreign_key "post_implications", "posts", column: "parent_post_id"
|
||||
add_foreign_key "post_similarities", "posts"
|
||||
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", "tags"
|
||||
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", "users", column: "created_by_user_id"
|
||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||
add_foreign_key "settings", "users"
|
||||
add_foreign_key "tag_implications", "tags"
|
||||
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
||||
add_foreign_key "tag_names", "languages", column: "language_code", primary_key: "code"
|
||||
add_foreign_key "tag_names", "scripts", column: "script_code", primary_key: "code"
|
||||
add_foreign_key "tag_names", "tag_names", column: "canonical_id"
|
||||
add_foreign_key "tag_names", "tags"
|
||||
add_foreign_key "tag_similarities", "tags"
|
||||
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 "tags", "tag_names"
|
||||
add_foreign_key "theatre_comments", "theatres"
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace :nico do
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
to_add = desired_tag_ids - current_tag_ids
|
||||
@@ -30,28 +30,9 @@ namespace :nico do
|
||||
end
|
||||
end
|
||||
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).find_each(&:destroy!)
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||
pt.discard_by!(nil)
|
||||
end
|
||||
|
||||
def sync_post_external_tags! post, desired_external_tag_ids, current_external_tag_ids: nil
|
||||
current_external_tag_ids ||=
|
||||
PostExternalTag.where(post_id: post.id).pluck(:external_tag_id).to_set
|
||||
desired_external_tag_ids = desired_external_tag_ids.compact.to_set
|
||||
|
||||
to_add = desired_external_tag_ids - current_external_tag_ids
|
||||
to_remove = current_external_tag_ids - desired_external_tag_ids
|
||||
|
||||
ExternalTag.where(id: to_add.to_a).find_each do |external_tag|
|
||||
begin
|
||||
PostExternalTag.create!(post:, external_tag:)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
;
|
||||
end
|
||||
end
|
||||
|
||||
PostExternalTag
|
||||
.where(post_id: post.id, external_tag_id: to_remove.to_a)
|
||||
.find_each(&:destroy!)
|
||||
end
|
||||
|
||||
mysql_user = ENV['MYSQL_USER']
|
||||
@@ -130,64 +111,58 @@ namespace :nico do
|
||||
sync_post_tags!(post, [Tag.tagme.id, Tag.bot.id, Tag.niconico.id, Tag.video.id])
|
||||
end
|
||||
|
||||
tags = post.tags
|
||||
# 既存のタグ Id. 集合
|
||||
kept_tag_ids = post.tags.pluck(:id).to_set
|
||||
|
||||
# 既存の外部タグ Id. 集合
|
||||
kept_external_tag_ids = post.external_tags.nico.pluck(:id).to_set
|
||||
|
||||
# 記載すべき外部タグ Id. のリスト
|
||||
desired_external_tag_ids = []
|
||||
kept_tag_ids = tags.pluck(:id).to_set
|
||||
# うち内部タグ Id. 集合
|
||||
kept_non_nico_tag_ids = tags.not_nico.pluck(:id).to_set
|
||||
|
||||
# 記載すべき外部タグ Id. および連携される内部タグ Id. のリスト
|
||||
desired_nico_tag_based_ids = []
|
||||
# 記載すべき内部タグ Id. のリスト
|
||||
desired_tag_ids = kept_tag_ids.to_a
|
||||
desired_non_nico_tag_ids = []
|
||||
|
||||
datum['tags'].each do |raw|
|
||||
name = TagNameSanitisationRule.sanitise("nico:#{ raw }").delete_prefix('nico:')
|
||||
tag = ExternalTag.find_or_create_by!(platform: :nico, name:)
|
||||
name = TagNameSanitisationRule.sanitise("nico:#{ raw }")
|
||||
tag = Tag.find_or_create_by_tag_name!(name, category: :nico)
|
||||
|
||||
unless tag.nico_tag_versions.exists?
|
||||
NicoTagVersionRecorder.record!(external_tag: tag,
|
||||
event_type: :create,
|
||||
created_by_user: nil)
|
||||
end
|
||||
event_type = tag.nico_tag_versions.exists? ? :update : :create
|
||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil)
|
||||
|
||||
desired_external_tag_ids << tag.id
|
||||
desired_nico_tag_based_ids << tag.id
|
||||
|
||||
# 新たに記載される外部タグと連携される内部タグを記載
|
||||
# 連携タグは記載すれども消除せず.
|
||||
unless tag.id.in?(kept_external_tag_ids)
|
||||
desired_tag_ids.concat(tag.linked_tags.pluck(:id))
|
||||
unless tag.id.in?(kept_tag_ids)
|
||||
linked_ids = tag.linked_tags.pluck(:id)
|
||||
desired_non_nico_tag_ids.concat(linked_ids)
|
||||
desired_nico_tag_based_ids.concat(linked_ids)
|
||||
end
|
||||
end
|
||||
|
||||
deerjikist = Deerjikist.find_by(platform: :nico, code: datum['user'])
|
||||
if deerjikist
|
||||
desired_tag_ids << deerjikist.tag_id
|
||||
elsif !(Tag.where(id: kept_tag_ids).where(category: :deerjikist).exists?)
|
||||
desired_tag_ids << Tag.no_deerjikist.id
|
||||
desired_non_nico_tag_ids << deerjikist.tag_id
|
||||
desired_nico_tag_based_ids << deerjikist.tag_id
|
||||
elsif !(Tag.where(id: kept_non_nico_tag_ids).where(category: :deerjikist).exists?)
|
||||
desired_non_nico_tag_ids << Tag.no_deerjikist.id
|
||||
desired_nico_tag_based_ids << Tag.no_deerjikist.id
|
||||
end
|
||||
|
||||
desired_external_tag_ids.uniq!
|
||||
desired_tag_ids.uniq!
|
||||
desired_nico_tag_based_ids.uniq!
|
||||
|
||||
# 外部タグの記載に際しては “bot 操作” タグを記載しなぃ.
|
||||
if kept_tag_ids != desired_tag_ids.to_set
|
||||
desired_tag_ids << Tag.bot.id
|
||||
desired_tag_ids.uniq!
|
||||
desired_all_tag_ids = kept_non_nico_tag_ids.to_a + desired_nico_tag_based_ids
|
||||
desired_non_nico_tag_ids.concat(kept_non_nico_tag_ids.to_a)
|
||||
desired_non_nico_tag_ids.uniq!
|
||||
if kept_non_nico_tag_ids != desired_non_nico_tag_ids.to_set
|
||||
desired_all_tag_ids << Tag.bot.id
|
||||
end
|
||||
desired_all_tag_ids.uniq!
|
||||
|
||||
tags_changed =
|
||||
kept_tag_ids != desired_tag_ids.to_set ||
|
||||
kept_external_tag_ids != desired_external_tag_ids.to_set
|
||||
|
||||
sync_post_tags!(post, desired_tag_ids, current_tag_ids: kept_tag_ids)
|
||||
sync_post_external_tags!(post, desired_external_tag_ids,
|
||||
current_external_tag_ids: kept_external_tag_ids)
|
||||
sync_post_tags!(post, desired_all_tag_ids, current_tag_ids: kept_tag_ids)
|
||||
|
||||
if post_created
|
||||
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: nil)
|
||||
elsif post_changed || tags_changed
|
||||
elsif post_changed || kept_tag_ids != desired_all_tag_ids.to_set
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: nil)
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil)
|
||||
end
|
||||
|
||||
@@ -1,124 +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
|
||||
# Reproduce the historical schema required by these pre-multilingual migrations.
|
||||
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,191 +0,0 @@
|
||||
require 'rails_helper'
|
||||
require_relative '../../db/migrate/20260921040000_create_external_tags'
|
||||
require_relative '../../db/migrate/20260921050000_create_post_external_tags'
|
||||
require_relative '../../db/migrate/20260921060000_change_foreign_key_on_nico_tag_relations'
|
||||
require_relative '../../db/migrate/20260921230000_migrate_external_tags'
|
||||
|
||||
RSpec.describe 'external tag migrations' do
|
||||
self.use_transactional_tests = false
|
||||
|
||||
before do
|
||||
record_class = Class.new(ActiveRecord::Base) do
|
||||
self.abstract_class = true
|
||||
end
|
||||
stub_const('ExternalTagMigrationRecord', record_class)
|
||||
config = ActiveRecord::Base.connection_db_config.configuration_hash
|
||||
@database = "btrc_hub_test_external_tags_#{ 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) }")
|
||||
version_class = Class.new(record_class) do
|
||||
self.table_name = 'post_versions'
|
||||
end
|
||||
stub_const('MigrateExternalTags::MigrationPostVersion', version_class)
|
||||
create_legacy_tables
|
||||
seed_legacy_records
|
||||
end
|
||||
|
||||
after do
|
||||
@connection.drop_database(@database) if @database_created
|
||||
ensure
|
||||
ExternalTagMigrationRecord.remove_connection
|
||||
end
|
||||
|
||||
def migrate klass
|
||||
migration = klass.new
|
||||
allow(migration).to receive(:connection).and_return(@connection)
|
||||
migration.suppress_messages { migration.migrate(:up) }
|
||||
end
|
||||
|
||||
def create_legacy_tables
|
||||
@connection.create_table(:tag_names) { |t| t.string :name }
|
||||
@connection.create_table(:tags) do |t|
|
||||
t.references :tag_name, foreign_key: true
|
||||
t.string :category
|
||||
t.integer :post_count
|
||||
t.datetime :created_at
|
||||
end
|
||||
@connection.create_table(:posts)
|
||||
@connection.create_table(:post_tags) do |t|
|
||||
t.references :post, foreign_key: true
|
||||
t.references :tag, foreign_key: true
|
||||
t.datetime :created_at
|
||||
end
|
||||
@connection.create_table(:nico_tag_versions) do |t|
|
||||
t.bigint :tag_id
|
||||
t.integer :version_no
|
||||
t.string :name
|
||||
t.datetime :created_at
|
||||
end
|
||||
@connection.create_table(:nico_tag_relations) do |t|
|
||||
t.references :tag, foreign_key: true
|
||||
t.references :nico_tag, foreign_key: { to_table: :tags }
|
||||
end
|
||||
[:post_tag_sections, :materials, :theatre_skip_event_tags].each do |table|
|
||||
@connection.create_table(table) { |t| t.references :tag, foreign_key: true }
|
||||
end
|
||||
@connection.create_table(:tag_implications) do |t|
|
||||
t.references :tag, foreign_key: true
|
||||
t.references :parent_tag, foreign_key: { to_table: :tags }
|
||||
end
|
||||
@connection.create_table(:tag_similarities) do |t|
|
||||
t.references :tag, foreign_key: true
|
||||
t.references :target_tag, foreign_key: { to_table: :tags }
|
||||
end
|
||||
@connection.create_table(:post_versions) { |t| t.json :tags_json, null: false }
|
||||
@connection.add_check_constraint(:post_versions, 'JSON_VALID(tags_json)',
|
||||
name: 'chk_post_versions_tags_json_schema')
|
||||
end
|
||||
|
||||
def seed_legacy_records
|
||||
@connection.execute(<<~SQL)
|
||||
INSERT INTO tag_names (id, name) VALUES (1, 'internal'), (2, 'nico:raw tag[]');
|
||||
SQL
|
||||
@connection.execute(<<~SQL)
|
||||
INSERT INTO tags (id, tag_name_id, category, post_count, created_at) VALUES
|
||||
(1, 1, 'general', 1, '2026-09-01'), (2, 2, 'nico', 1, '2026-09-02')
|
||||
SQL
|
||||
@connection.execute('INSERT INTO posts (id) VALUES (1)')
|
||||
@connection.execute(<<~SQL)
|
||||
INSERT INTO post_tags (post_id, tag_id, created_at) VALUES
|
||||
(1, 1, '2026-09-03'), (1, 2, '2026-09-04')
|
||||
SQL
|
||||
@connection.execute(<<~SQL)
|
||||
INSERT INTO nico_tag_versions (tag_id, version_no, name, created_at) VALUES
|
||||
(2, 1, 'nico:raw tag[]', '2026-09-02'),
|
||||
(3, 1, 'nico:deleted', '2026-09-01'),
|
||||
(3, 2, 'nico:deleted_later', '2026-09-02')
|
||||
SQL
|
||||
@connection.execute('INSERT INTO nico_tag_relations (tag_id, nico_tag_id) VALUES (1, 2)')
|
||||
@connection.execute(<<~SQL)
|
||||
INSERT INTO tag_similarities (tag_id, target_tag_id) VALUES (1, 1), (1, 2), (2, 1)
|
||||
SQL
|
||||
@connection.execute('INSERT INTO theatre_skip_event_tags (tag_id) VALUES (1), (2)')
|
||||
@legacy_internal = { 'id' => 1, 'version_no' => 2,
|
||||
'name' => 'internal', 'category' => 'general',
|
||||
'sections' => [{ 'begin_ms' => 1000, 'end_ms' => nil }] }
|
||||
snapshots = [@legacy_internal,
|
||||
{ 'id' => 2, 'category' => 'nico' },
|
||||
{ 'id' => 3, 'category' => 'nico' }]
|
||||
@connection.execute(<<~SQL)
|
||||
INSERT INTO post_versions (tags_json) VALUES
|
||||
(#{ @connection.quote(snapshots.to_json) }), ('[]')
|
||||
SQL
|
||||
end
|
||||
|
||||
def prepare_external_tables
|
||||
migrate(CreateExternalTags)
|
||||
migrate(CreatePostExternalTags)
|
||||
migrate(ChangeForeignKeyOnNicoTagRelations)
|
||||
end
|
||||
|
||||
it 'preserves ids, raw names, counts, timestamps, post links and historical-only tags' do
|
||||
prepare_external_tables
|
||||
|
||||
rows = @connection.select_rows(<<~SQL)
|
||||
SELECT id, platform, name, post_count, DATE_FORMAT(created_at, '%Y-%m-%d')
|
||||
FROM external_tags ORDER BY id
|
||||
SQL
|
||||
expect(rows).to eq([
|
||||
[2, 'nico', 'raw tag[]', 1, '2026-09-02'],
|
||||
[3, 'nico', 'deleted', 0, '2026-09-01']])
|
||||
expect(@connection.select_rows(<<~SQL)).to eq([[1, 2, '2026-09-04']])
|
||||
SELECT post_id, external_tag_id, DATE_FORMAT(created_at, '%Y-%m-%d')
|
||||
FROM post_external_tags
|
||||
SQL
|
||||
expect(@connection.foreign_key_exists?(:nico_tag_relations, :external_tags,
|
||||
column: :nico_tag_id)).to be(true)
|
||||
expect(@connection.foreign_key_exists?(:nico_tag_relations, :tags,
|
||||
column: :tag_id)).to be(true)
|
||||
end
|
||||
|
||||
it 'converts snapshots and removes only obsolete internal rows and derived links' do
|
||||
prepare_external_tables
|
||||
migrate(MigrateExternalTags)
|
||||
|
||||
expect(@connection.select_values('SELECT id FROM tags')).to eq([1])
|
||||
expect(@connection.select_values('SELECT id FROM tag_names')).to eq([1])
|
||||
expect(@connection.select_values('SELECT tag_id FROM post_tags')).to eq([1])
|
||||
expect(@connection.select_rows('SELECT tag_id, nico_tag_id FROM nico_tag_relations'))
|
||||
.to eq([[1, 2]])
|
||||
expect(@connection.select_rows('SELECT tag_id, target_tag_id FROM tag_similarities'))
|
||||
.to eq([[1, 1]])
|
||||
expect(@connection.select_values('SELECT tag_id FROM theatre_skip_event_tags')).to eq([1])
|
||||
expect(@connection.select_value('SELECT COUNT(*) FROM nico_tag_versions')).to eq(3)
|
||||
versions = MigrateExternalTags::MigrationPostVersion.order(:id)
|
||||
expect(versions.first.tags_json).to eq([
|
||||
@legacy_internal.except('id').merge('tag_id' => 1),
|
||||
{ 'external_tag_id' => 2 }, { 'external_tag_id' => 3 }])
|
||||
expect(versions.last.tags_json).to eq([])
|
||||
|
||||
invalid_snapshots = [@legacy_internal,
|
||||
{ 'external_tag_id' => 0 },
|
||||
{ 'external_tag_id' => 2, 'tag_id' => 1 },
|
||||
{ 'external_tag_id' => 2, 'name' => 'extra' },
|
||||
{ 'tag_id' => 1, 'version_no' => 1 }]
|
||||
invalid_snapshots.each do |snapshot|
|
||||
expect {
|
||||
MigrateExternalTags::MigrationPostVersion.create!(tags_json: [snapshot])
|
||||
}.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
||||
end
|
||||
expect { MigrateExternalTags.new.down }
|
||||
.to raise_error(ActiveRecord::IrreversibleMigration)
|
||||
end
|
||||
|
||||
{ post_tag_sections: '(tag_id) VALUES (2)',
|
||||
materials: '(tag_id) VALUES (2)',
|
||||
tag_implications: '(tag_id, parent_tag_id) VALUES (1, 2)' }.each do |table, values|
|
||||
it "aborts before deleting data when #{ table } references a legacy nico tag" do
|
||||
prepare_external_tables
|
||||
@connection.execute("INSERT INTO #{ table } #{ values }")
|
||||
|
||||
expect { migrate(MigrateExternalTags) }.to raise_error(RuntimeError, /#{ table }/)
|
||||
|
||||
expect(@connection.select_values('SELECT id FROM tags ORDER BY id')).to eq([1, 2])
|
||||
expect(MigrateExternalTags::MigrationPostVersion.first.tags_json.first)
|
||||
.to eq(@legacy_internal)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
FactoryBot.define do
|
||||
factory :external_tag do
|
||||
platform { :nico }
|
||||
sequence(:name) { |n| "external_tag_#{ n }" }
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,5 @@
|
||||
FactoryBot.define do
|
||||
factory :tag_name do
|
||||
language_code { 'ja' }
|
||||
script_code { 'Jpan' }
|
||||
primary_flg { true }
|
||||
auto_generated { false }
|
||||
sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" }
|
||||
|
||||
trait :alias do
|
||||
primary_flg { false }
|
||||
end
|
||||
name { "tag-#{SecureRandom.hex(4)}" }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
FactoryBot.define do
|
||||
factory :tag do
|
||||
transient do
|
||||
primary_name { nil }
|
||||
primary_tag_name do
|
||||
attributes = primary_name.nil? ? { } : { name: primary_name }
|
||||
build(:tag_name, **attributes)
|
||||
end
|
||||
name { nil }
|
||||
end
|
||||
|
||||
category { :general }
|
||||
post_count { 0 }
|
||||
association :tag_name
|
||||
|
||||
after(:build) do |tag, evaluator|
|
||||
evaluator.primary_tag_name.primary_flg = true
|
||||
tag.tag_names.target << evaluator.primary_tag_name
|
||||
tag.name = evaluator.name if evaluator.name.present?
|
||||
end
|
||||
|
||||
before(:create) do |tag, evaluator|
|
||||
evaluator.primary_tag_name.save!
|
||||
# Compatibility write only; ownership is established through tag_id below.
|
||||
tag.tag_name_id = evaluator.primary_tag_name.id
|
||||
trait :nico do
|
||||
category { :nico }
|
||||
transient do
|
||||
name { "nico:#{ SecureRandom.hex(4) }" }
|
||||
end
|
||||
|
||||
after(:create) do |tag, evaluator|
|
||||
evaluator.primary_tag_name.update!(tag:, primary_flg: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ExternalTag, type: :model do
|
||||
it 'preserves names without internal tag sanitisation or TagName records' do
|
||||
external = nil
|
||||
|
||||
expect {
|
||||
external = described_class.create!(platform: :nico, name: 'raw tag[]')
|
||||
}.not_to change(TagName, :count)
|
||||
|
||||
expect(external.reload.name).to eq('raw tag[]')
|
||||
end
|
||||
|
||||
it 'deletes post associations without deleting posts or version history' do
|
||||
external = create(:external_tag)
|
||||
post = create(:post)
|
||||
PostExternalTag.create!(post:, external_tag: external)
|
||||
version = NicoTagVersionRecorder.record!(
|
||||
external_tag: external, event_type: :create, created_by_user: nil)
|
||||
|
||||
external.destroy!
|
||||
|
||||
expect(PostExternalTag.where(external_tag_id: external.id)).to be_empty
|
||||
expect(post.reload.external_tags).to be_empty
|
||||
expect(version.reload.tag_id).to eq(external.id)
|
||||
end
|
||||
|
||||
it 'deletes external links without deleting linked internal tags' do
|
||||
external = create(:external_tag)
|
||||
tag = create(:tag)
|
||||
NicoTagRelation.create!(nico_tag: external, tag:)
|
||||
|
||||
external.destroy!
|
||||
|
||||
expect(NicoTagRelation.where(nico_tag_id: external.id)).to be_empty
|
||||
expect(Tag.exists?(tag.id)).to be(true)
|
||||
end
|
||||
end
|
||||
@@ -1,71 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Locale, type: :model do
|
||||
def prepare_french_reference!
|
||||
Language.find_or_create_by!(code: 'fr') { _1.name = 'French' }
|
||||
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
|
||||
end
|
||||
|
||||
def create_french_locale!
|
||||
prepare_french_reference!
|
||||
described_class.create!(code: 'fr', language_code: 'fr',
|
||||
script_code: 'Latn', name: 'French')
|
||||
end
|
||||
|
||||
it 'generates a primary name for every existing tag using the name generator' do
|
||||
first = create(:tag, primary_name: 'first_existing_tag')
|
||||
second = create(:tag, primary_name: 'second_existing_tag')
|
||||
allow(TagName).to receive(:generate_name).and_call_original
|
||||
allow(TagName).to receive(:generate_name)
|
||||
.with(kind_of(described_class), first, primary_tag_name_for(first, 'ja').name)
|
||||
.and_return('name_from_generator')
|
||||
|
||||
locale = create_french_locale!
|
||||
|
||||
expect(TagName).to have_received(:generate_name)
|
||||
.with(locale, first, primary_tag_name_for(first, 'ja').name)
|
||||
expect(TagName).to have_received(:generate_name)
|
||||
.with(locale, second, primary_tag_name_for(second, 'ja').name)
|
||||
expect(first.tag_names.find_by!(language_code: 'fr', primary_flg: true))
|
||||
.to have_attributes(name: 'name_from_generator', tag_id: first.id,
|
||||
language_code: 'fr', script_code: 'Latn',
|
||||
primary_flg: true)
|
||||
expect(second.tag_names.find_by!(language_code: 'fr', primary_flg: true))
|
||||
.to have_attributes(tag_id: second.id, language_code: 'fr',
|
||||
script_code: 'Latn', primary_flg: true)
|
||||
expect(TagName.where(language_code: 'fr', primary_flg: true,
|
||||
tag_id: [first.id, second.id]).count).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not duplicate an existing primary name in the new language' do
|
||||
prepare_french_reference!
|
||||
tag = create(:tag, primary_name: 'already_named')
|
||||
existing = create(:tag_name, name: 'nom_existant', tag:,
|
||||
language_code: 'fr', script_code: 'Latn')
|
||||
|
||||
create_french_locale!
|
||||
|
||||
expect(TagName.where(tag_id: tag.id, language_code: 'fr', primary_flg: true))
|
||||
.to contain_exactly(existing)
|
||||
expect(existing.reload).to have_attributes(
|
||||
tag_id: tag.id, language_code: 'fr', script_code: 'Latn', primary_flg: true)
|
||||
end
|
||||
|
||||
it 'creates a primary name when the language has only an alias' do
|
||||
prepare_french_reference!
|
||||
tag = create(:tag, primary_name: 'alias_only_tag')
|
||||
alias_name = create(:tag_name, :alias, name: 'alias_fr',
|
||||
tag:,
|
||||
language_code: 'fr', script_code: 'Latn')
|
||||
|
||||
create_french_locale!
|
||||
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: tag.id,
|
||||
language_code: 'fr', script_code: 'Latn', primary_flg: false)
|
||||
generated = TagName.find_by!(tag_id: tag.id, language_code: 'fr', primary_flg: true)
|
||||
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'fr',
|
||||
script_code: 'Latn', primary_flg: true)
|
||||
expect(TagName.where(tag_id: tag.id, language_code: 'fr', primary_flg: true).count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe MaterialExportItem, type: :model do
|
||||
let(:user) { create(:user, :member) }
|
||||
let(:tag) { create(:tag, primary_name: 'export_item', category: :material) }
|
||||
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'export_item'), category: :material) }
|
||||
let(:material) do
|
||||
Material.create!(tag:, url: 'https://example.com/material',
|
||||
created_by_user: user, updated_by_user: user)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe NicoTagRelation, type: :model do
|
||||
it 'does not constrain the ExternalTag association to the nico platform' do
|
||||
external_tag = create(:external_tag)
|
||||
tag = create(:tag)
|
||||
allow(external_tag).to receive(:platform).and_return('registered_external')
|
||||
allow(external_tag).to receive(:nico?).and_return(false)
|
||||
|
||||
expect {
|
||||
described_class.create!(nico_tag: external_tag, tag:)
|
||||
}.to change(described_class, :count).by(1)
|
||||
expect(tag.linked_nico_tags).to contain_exactly(external_tag)
|
||||
end
|
||||
|
||||
it 'rejects an internal Tag through the external association type' do
|
||||
expect {
|
||||
described_class.new(nico_tag: create(:tag), tag: create(:tag))
|
||||
}.to raise_error(ActiveRecord::AssociationTypeMismatch)
|
||||
end
|
||||
|
||||
it 'rejects an ExternalTag through the internal association type' do
|
||||
expect {
|
||||
described_class.new(nico_tag: create(:external_tag), tag: create(:external_tag))
|
||||
}.to raise_error(ActiveRecord::AssociationTypeMismatch)
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PostExternalTag, type: :model do
|
||||
it 'exposes both sides and enforces a unique post and external tag pair' do
|
||||
post = create(:post)
|
||||
external_tag = create(:external_tag)
|
||||
described_class.create!(post:, external_tag:)
|
||||
|
||||
expect(post.external_tags).to contain_exactly(external_tag)
|
||||
expect(external_tag.posts).to contain_exactly(post)
|
||||
expect { described_class.create!(post:, external_tag:) }
|
||||
.to raise_error(ActiveRecord::RecordNotUnique)
|
||||
end
|
||||
|
||||
it 'removes associations when the post is deleted while retaining the external tag' do
|
||||
post = create(:post)
|
||||
external_tag = create(:external_tag)
|
||||
described_class.create!(post:, external_tag:)
|
||||
|
||||
post.destroy!
|
||||
|
||||
expect(described_class.where(post_id: post.id)).to be_empty
|
||||
expect(external_tag.reload.posts).to be_empty
|
||||
end
|
||||
end
|
||||
@@ -6,25 +6,6 @@ RSpec.describe Post, type: :model do
|
||||
PostUrlSanitisationRule.unscoped.delete_all
|
||||
end
|
||||
|
||||
describe '#snapshot_tags_json' do
|
||||
it 'keeps internal snapshots and external identifiers distinct, even with the same id' do
|
||||
post = create(:post)
|
||||
tag = create(:tag)
|
||||
external = create(:external_tag, id: tag.id)
|
||||
create(:post_tag, post:, tag:)
|
||||
create(:post_tag_section, post:, tag:, begin_ms: 2000, end_ms: nil)
|
||||
PostExternalTag.create!(post:, external_tag: external)
|
||||
|
||||
expect(post.snapshot_tags_json).to eq([
|
||||
{ 'tag_id' => tag.id,
|
||||
'version_no' => tag.version_no,
|
||||
'name' => primary_tag_name_for(tag, 'ja').name,
|
||||
'category' => tag.category,
|
||||
'sections' => [{ 'begin_ms' => 2000, 'end_ms' => nil }] },
|
||||
{ 'external_tag_id' => external.id }])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'URL normalisation' do
|
||||
it 'normalises the HTTP URL before applying sanitisation rules' do
|
||||
PostUrlSanitisationRule.create!(
|
||||
|
||||
@@ -1,73 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
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
|
||||
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
|
||||
post_tag = create(:post_tag)
|
||||
section = create(:post_tag_section,
|
||||
@@ -80,25 +12,18 @@ RSpec.describe PostTag, type: :model do
|
||||
end
|
||||
|
||||
it 'does not load sections for another tag on the same post' do
|
||||
post_tag = create(:post_tag)
|
||||
post = post_tag.post
|
||||
post = create(:post)
|
||||
tag = create(:tag)
|
||||
other_tag = create(:tag)
|
||||
|
||||
own_section = create(:post_tag_section,
|
||||
post:,
|
||||
tag: post_tag.tag,
|
||||
begin_ms: 1000,
|
||||
end_ms: 2000)
|
||||
|
||||
create(:post_tag, post:, tag: other_tag)
|
||||
|
||||
post_tag = create(:post_tag, post:, tag:)
|
||||
create(:post_tag_section,
|
||||
post:,
|
||||
tag: other_tag,
|
||||
begin_ms: 1000,
|
||||
end_ms: 2000)
|
||||
|
||||
expect(post_tag.reload.sections).to contain_exactly(own_section)
|
||||
expect(post_tag.sections).to be_empty
|
||||
end
|
||||
|
||||
it 'allows open-ended sections' do
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PostVersion, type: :model do
|
||||
let!(:tag_name) { create(:tag_name, name: 'post_version_spec_tag') }
|
||||
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
|
||||
let!(:tag_name) { TagName.create!(name: 'post_version_spec_tag') }
|
||||
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
|
||||
|
||||
let!(:post_record) do
|
||||
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
|
||||
@@ -19,7 +19,6 @@ RSpec.describe PostVersion, type: :model do
|
||||
url: post_record.url,
|
||||
thumbnail_base: post_record.thumbnail_base,
|
||||
tags: post_record.snapshot_tag_names.join(' '),
|
||||
tags_json: post_record.snapshot_tags_json,
|
||||
parent_post_ids: post_record.snapshot_parent_post_ids.join(' '),
|
||||
original_created_from: post_record.original_created_from,
|
||||
original_created_before: post_record.original_created_before,
|
||||
|
||||
@@ -2,8 +2,8 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe TagImplication, type: :model do
|
||||
it 'rejects a parent tag that would create a cycle' do
|
||||
child = create(:tag, primary_name: 'tag_implication_cycle_child')
|
||||
parent = create(:tag, primary_name: 'tag_implication_cycle_parent')
|
||||
child = create(:tag, name: 'tag_implication_cycle_child')
|
||||
parent = create(:tag, name: 'tag_implication_cycle_parent')
|
||||
|
||||
described_class.create!(tag: child, parent_tag: parent)
|
||||
|
||||
@@ -17,9 +17,9 @@ RSpec.describe TagImplication, type: :model do
|
||||
end
|
||||
|
||||
it 'terminates even when existing data already contains a cycle' do
|
||||
child = create(:tag, primary_name: 'tag_implication_existing_cycle_child')
|
||||
parent = create(:tag, primary_name: 'tag_implication_existing_cycle_parent')
|
||||
ancestor = create(:tag, primary_name: 'tag_implication_existing_cycle_ancestor')
|
||||
child = create(:tag, name: 'tag_implication_existing_cycle_child')
|
||||
parent = create(:tag, name: 'tag_implication_existing_cycle_parent')
|
||||
ancestor = create(:tag, name: 'tag_implication_existing_cycle_ancestor')
|
||||
|
||||
described_class.create!(tag: parent, parent_tag: ancestor)
|
||||
described_class.insert_all!(
|
||||
|
||||
@@ -34,41 +34,9 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
described_class.create!(priority: 10, source_pattern: '_', replacement: '')
|
||||
end
|
||||
|
||||
context 'when only another language has the sanitised name' do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'keeps both names when sanitisation produces a cross-language match' do
|
||||
japanese = create(:tag_name, name: 'foobar')
|
||||
english = create(:tag_name, name: 'temporary',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english.update_columns(name: 'foo_bar')
|
||||
|
||||
expect { described_class.apply! }.not_to change(TagName, :count)
|
||||
|
||||
expect(english.reload).to have_attributes(name: 'foobar', language_code: 'en')
|
||||
expect(japanese.reload).to have_attributes(name: 'foobar', language_code: 'ja')
|
||||
end
|
||||
|
||||
it 'renames without merging tags or deleting either language identity' do
|
||||
japanese = create(:tag, primary_name: 'foobar')
|
||||
english_name = create(:tag_name, name: 'temporary',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name)
|
||||
english_name.update_columns(name: 'foo_bar')
|
||||
|
||||
described_class.apply!
|
||||
|
||||
expect(english_name.reload).to have_attributes(
|
||||
name: 'foobar', language_code: 'en', tag_id: english.id)
|
||||
expect(primary_tag_name_for(japanese.reload, 'ja')).to have_attributes(
|
||||
name: 'foobar', language_code: 'ja', tag_id: japanese.id)
|
||||
expect(primary_tag_name_for(english.reload, 'en')).to eq(english_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no conflicting tag_name exists' do
|
||||
let!(:tag_name) do
|
||||
create(:tag_name, name: 'tmp').tap do |tn|
|
||||
TagName.create!(name: 'tmp').tap do |tn|
|
||||
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
@@ -79,72 +47,44 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a conflicting primary tag name exists' do
|
||||
let!(:existing) { create(:tag_name, name: 'foobar') }
|
||||
context 'when a conflicting canonical tag_name exists' do
|
||||
let!(:existing) { TagName.create!(name: 'foobar') }
|
||||
let!(:source) do
|
||||
create(:tag_name, name: 'tmp').tap do |tn|
|
||||
TagName.create!(name: 'tmp').tap do |tn|
|
||||
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
it 'deletes the source tag_name' do
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the source tag_name has a tag and the existing one has no tag' do
|
||||
let!(:existing) { create(:tag_name, name: 'foobar') }
|
||||
let!(:source_tag) { create(:tag, primary_name: 'tmp', category: :general) }
|
||||
let!(:source_tag_name_id) { primary_tag_name_for(source_tag, 'ja').id }
|
||||
let!(:existing) { TagName.create!(name: 'foobar') }
|
||||
let!(:source_tag) { create(:tag, name: 'tmp', category: :general) }
|
||||
let!(:source_tag_name_id) { source_tag.tag_name_id }
|
||||
|
||||
before do
|
||||
primary_tag_name_for(source_tag, 'ja').update_columns(
|
||||
name: 'foo_bar', updated_at: Time.current)
|
||||
source_tag.tag_name.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||
end
|
||||
|
||||
it 'moves the tag to the existing tag_name' do
|
||||
described_class.apply!
|
||||
expect(existing.reload).to have_attributes(
|
||||
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
|
||||
expect(primary_tag_name_for(source_tag, 'ja')).to eq(existing)
|
||||
expect(primary_tag_name_for(source_tag.reload, 'ja')).to eq(existing)
|
||||
expect(TagName.unscoped.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!(:existing_primary) { primary_tag_name_for(existing_tag, 'ja') }
|
||||
let!(:alias_name) do
|
||||
create(:tag_name, :alias, name: 'foobar', tag: existing_tag)
|
||||
end
|
||||
let!(:source) do
|
||||
create(:tag_name, 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 owning tag' do
|
||||
described_class.apply!
|
||||
|
||||
expect(TagName.unscoped.exists?(source.id)).to be(false)
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: existing_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(TagName.find_by!(tag_id: existing_tag.id,
|
||||
language_code: 'ja', primary_flg: true))
|
||||
.to eq(existing_primary)
|
||||
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
|
||||
expected_tag_name_id = existing.canonical_id || existing.id
|
||||
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
|
||||
expect(TagName.exists?(source_tag_name_id)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when both source and existing tag_names have tags' do
|
||||
let!(:existing_tn) { create(:tag_name, name: 'foobar') }
|
||||
let!(:existing_tag) { create(:tag, primary_tag_name: existing_tn, category: :general) }
|
||||
let!(:existing_tn) { TagName.create!(name: 'foobar') }
|
||||
let!(:existing_tag) { Tag.create!(tag_name: existing_tn, category: :general) }
|
||||
|
||||
let!(:source_tn) { create(:tag_name, name: 'tmp') }
|
||||
let!(:source_tag) { create(:tag, primary_tag_name: source_tn, category: :general) }
|
||||
let!(:source_tn) { TagName.create!(name: 'tmp') }
|
||||
let!(:source_tag) { Tag.create!(tag_name: source_tn, category: :general) }
|
||||
let!(:source_tag_name_id) { source_tn.id }
|
||||
|
||||
before do
|
||||
@@ -152,15 +92,13 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
end
|
||||
|
||||
it 'merges the source tag into the existing tag and deletes the source tag_name' do
|
||||
post = create(:post)
|
||||
PostTag.create!(post:, tag: source_tag)
|
||||
expect(TagName.find_by(name: 'foobar')&.tag&.id).to eq(existing_tag.id)
|
||||
expect(TagName.find_by(name: 'foo_bar')&.tag&.id).to eq(source_tag.id)
|
||||
|
||||
described_class.apply!
|
||||
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||
expect(post.reload.tags).to contain_exactly(existing_tag)
|
||||
expect(primary_tag_name_for(existing_tag.reload, 'ja').name).to eq('foobar')
|
||||
expect(Tag.exists?(source_tag.id)).to be(false)
|
||||
expect(TagName.exists?(source_tag.tag_name_id)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagName, type: :model do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'allows the same name in different languages' do
|
||||
create(:tag_name, name: 'shared_name')
|
||||
english = build(:tag_name, name: 'shared_name',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect(english).to be_valid
|
||||
expect { english.save! }.to change(described_class, :count).by(1)
|
||||
end
|
||||
|
||||
it 'rejects a duplicate name within the same language' do
|
||||
create(:tag_name, name: 'shared_name')
|
||||
duplicate = build(:tag_name, name: 'shared_name')
|
||||
|
||||
expect(duplicate).to be_invalid
|
||||
expect(duplicate.errors.of_kind?(:name, :taken)).to be(true)
|
||||
end
|
||||
|
||||
it 'creates Japanese factory names without requiring a tag' do
|
||||
name = create(:tag_name)
|
||||
|
||||
expect(name.reload).to have_attributes(
|
||||
language_code: 'ja', script_code: 'Jpan', primary_flg: true, tag_id: nil)
|
||||
end
|
||||
|
||||
it 'persists both sides of the representative tag factory association' do
|
||||
name = create(:tag_name)
|
||||
tag = create(:tag, primary_tag_name: name)
|
||||
|
||||
expect(name.reload).to have_attributes(
|
||||
tag_id: tag.id, language_code: 'ja', script_code: 'Jpan', primary_flg: true)
|
||||
expect(name.tag).to eq(tag)
|
||||
expect(described_class.find_by!(tag_id: tag.id, language_code: 'ja', primary_flg: true))
|
||||
.to eq(name)
|
||||
expect(tag.reload.tag_name_id).to eq(name.id)
|
||||
end
|
||||
|
||||
it 'builds a primary name without persisting the tag or its name' do
|
||||
tag = nil
|
||||
|
||||
expect {
|
||||
tag = build(:tag, primary_name: 'built_primary')
|
||||
}.to change(Tag, :count).by(0).and change(described_class, :count).by(0)
|
||||
|
||||
expect(tag).to be_new_record
|
||||
expect(tag.tag_names.target).to contain_exactly(have_attributes(
|
||||
name: 'built_primary', language_code: 'ja', script_code: 'Jpan',
|
||||
primary_flg: true))
|
||||
end
|
||||
|
||||
it 'persists an alias owned by the same tag as its primary name' do
|
||||
primary_name = create(:tag_name)
|
||||
tag = create(:tag, primary_tag_name: primary_name)
|
||||
alias_name = create(:tag_name, :alias, name: 'valid_alias', tag:)
|
||||
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: tag.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
primary = described_class.find_by!(tag_id: alias_name.tag_id,
|
||||
language_code: alias_name.language_code,
|
||||
primary_flg: true)
|
||||
expect(primary).to eq(primary_name)
|
||||
end
|
||||
|
||||
describe '.canonicalise' do
|
||||
it 'leaves detached aliases unchanged' do
|
||||
create(:tag_name, :alias, name: 'detached_alias')
|
||||
|
||||
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
|
||||
.to eq(['detached_alias'])
|
||||
end
|
||||
|
||||
it 'leaves an alias unchanged when its language has no primary' do
|
||||
tag = create(:tag, primary_name: '日本語のみ')
|
||||
create(:tag_name, :alias, tag:, name: 'orphan_english_alias',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect(described_class.canonicalise(locale, ['orphan_english_alias']))
|
||||
.to eq(['orphan_english_alias'])
|
||||
end
|
||||
|
||||
it 'resolves the primary name of the alias language on the same tag' do
|
||||
tag = create(:tag, primary_name: '日本語正本')
|
||||
english_primary = create(:tag_name, tag:, name: 'english_primary',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english_alias = create(:tag_name, :alias, tag:, name: 'english_alias',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
representative_id = tag.tag_name_id
|
||||
|
||||
primary = described_class.find_by!(tag_id: english_alias.tag_id,
|
||||
language_code: english_alias.language_code,
|
||||
primary_flg: true)
|
||||
|
||||
expect(primary).to eq(english_primary)
|
||||
expect(described_class.canonicalise(locale, [english_alias.name]))
|
||||
.to eq([english_primary.name])
|
||||
expect(tag.reload.tag_name_id).to eq(representative_id)
|
||||
end
|
||||
|
||||
it 'resolves only aliases in the requested language' do
|
||||
japanese = create(:tag, primary_name: 'japanese_canonical')
|
||||
english_name = create(:tag_name, name: 'english_canonical',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name)
|
||||
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: japanese)
|
||||
english_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: english,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect(japanese_alias.reload).to have_attributes(
|
||||
tag_id: japanese.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
expect(english_alias.reload).to have_attributes(
|
||||
tag_id: english.id,
|
||||
primary_flg: false, language_code: 'en')
|
||||
expect(described_class.canonicalise(locale, ['shared_alias']))
|
||||
.to eq(['english_canonical'])
|
||||
expect(described_class.canonicalise(Locale.nipponese, ['shared_alias']))
|
||||
.to eq(['japanese_canonical'])
|
||||
end
|
||||
end
|
||||
end
|
||||
+131
-382
@@ -1,184 +1,129 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Tag, type: :model do
|
||||
describe 'external tag separation' do
|
||||
['nico:reserved', 'NiCo:reserved'].each do |name|
|
||||
it "rejects the reserved prefix #{ name } for internal tags" do
|
||||
tag = build(:tag, primary_name: name)
|
||||
|
||||
expect(tag).to be_invalid
|
||||
expect(tag.errors[:name]).to be_present
|
||||
expect {
|
||||
described_class.normalise_tags!(Locale.nipponese, [name])
|
||||
}.to raise_error(Tag::NicoTagNormalisationError)
|
||||
end
|
||||
end
|
||||
|
||||
it 'finds external links by the internal tag id even when ids differ' do
|
||||
tag = create(:tag)
|
||||
external = create(:external_tag, id: tag.id + 10_000)
|
||||
# The migration retains existing links without running model validation.
|
||||
NicoTagRelation.insert_all!([{ tag_id: tag.id, nico_tag_id: external.id }])
|
||||
|
||||
expect(tag.linked_nico_tags).to contain_exactly(external)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.normalise_tags!' do
|
||||
it 'canonicalises aliases in the supplied language only' do
|
||||
Language.find_or_create_by!(code: 'en') { _1.name = 'English' }
|
||||
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
|
||||
Locale.insert_all!([
|
||||
{ code: 'en', language_code: 'en', script_code: 'Latn',
|
||||
name: 'English', created_at: Time.current }]) unless Locale.exists?(code: 'en')
|
||||
english_locale = Locale.find('en')
|
||||
japanese = create(:tag, primary_name: 'japanese_canonical')
|
||||
english_name = create(:tag_name, name: 'english_canonical',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name)
|
||||
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: japanese)
|
||||
english_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: english,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
expect(japanese_alias.reload).to have_attributes(
|
||||
tag_id: japanese.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
expect(english_alias.reload).to have_attributes(
|
||||
tag_id: english.id,
|
||||
primary_flg: false, language_code: 'en')
|
||||
expect(described_class.normalise_tags!(
|
||||
english_locale, ['shared_alias'],
|
||||
with_tagme: false, with_no_deerjikist: false)).to eq([english])
|
||||
expect(described_class.normalise_tags!(
|
||||
Locale.nipponese, ['shared_alias'],
|
||||
with_tagme: false, with_no_deerjikist: false)).to eq([japanese])
|
||||
end
|
||||
|
||||
it 'rejects deprecated tags when deny_deprecated is enabled' do
|
||||
tag_name = create(:tag_name, name: 'normalise deprecated tag')
|
||||
deprecated_tag = create(:tag,
|
||||
primary_tag_name: tag_name,
|
||||
tag_name = TagName.create!(name: 'normalise deprecated tag')
|
||||
deprecated_tag = Tag.create!(
|
||||
tag_name:,
|
||||
category: :general,
|
||||
deprecated_at: 1.day.from_now)
|
||||
deprecated_at: 1.day.from_now
|
||||
)
|
||||
|
||||
expect {
|
||||
described_class.normalise_tags!(Locale.nipponese,
|
||||
[primary_tag_name_for(deprecated_tag, 'ja').name],
|
||||
deny_deprecated: true)
|
||||
described_class.normalise_tags!(
|
||||
[deprecated_tag.name],
|
||||
deny_deprecated: true
|
||||
)
|
||||
}.to raise_error(Tag::DeprecatedTagNormalisationError) { |error|
|
||||
expect(error.tag_names).to eq([primary_tag_name_for(deprecated_tag, 'ja').name])
|
||||
expect(error.tag_names).to eq([deprecated_tag.name])
|
||||
}
|
||||
end
|
||||
|
||||
it 'rejects invalid section literals instead of treating them as zero' do
|
||||
expect {
|
||||
described_class.normalise_tags!(Locale.nipponese,
|
||||
described_class.normalise_tags!(
|
||||
['normalise_invalid_section[1:aa-2:00]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
}.to raise_error(Tag::SectionLiteralParseError)
|
||||
end
|
||||
|
||||
it 'parses open-ended section literals' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||
end
|
||||
|
||||
it 'parses omitted begin as zero' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[-1:00]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
|
||||
end
|
||||
|
||||
it 'treats fully open section literals as plain tags' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[-]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections)[tag.id]).to be_nil
|
||||
end
|
||||
|
||||
it 'treats [0:00-] as a plain tag' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[0:00-]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections)[tag.id]).to be_nil
|
||||
end
|
||||
|
||||
it 'expands zero-width sections to one millisecond' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-1:00]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
|
||||
end
|
||||
|
||||
it 'swaps reversed section boundaries' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[2:00-1:00]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
|
||||
end
|
||||
|
||||
it 'merges open-ended sections over later bounded sections' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-][2:00-3:00]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||
end
|
||||
|
||||
it 'merges adjacent bounded and open-ended sections' do
|
||||
result = described_class.normalise_tags!(Locale.nipponese,
|
||||
result = described_class.normalise_tags!(
|
||||
['伊地知ニジカ[1:00-3:00][3:00-]'],
|
||||
with_sections: true)
|
||||
with_sections: true
|
||||
)
|
||||
|
||||
tag = result.fetch(:tags).find do |candidate|
|
||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
||||
end
|
||||
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.expand_parent_tags' do
|
||||
it 'expands through multiple deprecated parents to an active ancestor' do
|
||||
child = create(:tag, primary_name: 'expand_child')
|
||||
deprecated_parent = create(:tag,
|
||||
primary_name: 'expand_deprecated_parent',
|
||||
deprecated_at: Time.current)
|
||||
deprecated_grandparent = create(:tag,
|
||||
primary_name: 'expand_deprecated_grandparent',
|
||||
deprecated_at: Time.current)
|
||||
active_ancestor = create(:tag, primary_name: 'expand_active_ancestor')
|
||||
child = create(:tag, name: 'expand_child')
|
||||
deprecated_parent = create(
|
||||
:tag,
|
||||
name: 'expand_deprecated_parent',
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
deprecated_grandparent = create(
|
||||
:tag,
|
||||
name: 'expand_deprecated_grandparent',
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
active_ancestor = create(:tag, name: 'expand_active_ancestor')
|
||||
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
|
||||
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
|
||||
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
|
||||
@@ -195,8 +140,8 @@ RSpec.describe Tag, type: :model do
|
||||
end
|
||||
|
||||
it 'terminates when implications contain a cycle' do
|
||||
first = create(:tag, primary_name: 'expand_cycle_first')
|
||||
second = create(:tag, primary_name: 'expand_cycle_second')
|
||||
first = create(:tag, name: 'expand_cycle_first')
|
||||
second = create(:tag, name: 'expand_cycle_second')
|
||||
TagImplication.create!(tag: first, parent_tag: second)
|
||||
now = Time.current
|
||||
TagImplication.insert_all!(
|
||||
@@ -214,170 +159,45 @@ RSpec.describe Tag, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find_or_create_by_tag_name!' do
|
||||
context 'with an explicit locale' do
|
||||
include_context 'English locale'
|
||||
describe 'deprecated validation' do
|
||||
it 'rejects deprecated nico tags' do
|
||||
tag = build(
|
||||
:tag,
|
||||
name: 'nico:deprecated_validation',
|
||||
category: :nico,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
|
||||
it 'creates the representative name with locale attributes and ownership' do
|
||||
tag = described_class.find_or_create_by_tag_name!(
|
||||
locale, 'english_name', category: :character)
|
||||
|
||||
expect(primary_tag_name_for(tag, 'en')).to have_attributes(
|
||||
language_code: 'en', script_code: 'Latn',
|
||||
primary_flg: true, tag_id: tag.id)
|
||||
expect(tag.category).to eq('character')
|
||||
end
|
||||
|
||||
it 'creates separate tag identities for the same name in different languages' do
|
||||
japanese = described_class.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'same_name', category: :general)
|
||||
english = described_class.find_or_create_by_tag_name!(
|
||||
locale, 'same_name', category: :character)
|
||||
|
||||
expect(english).not_to eq(japanese)
|
||||
expect(primary_tag_name_for(english, 'en')).to have_attributes(
|
||||
name: 'same_name', tag_id: english.id, language_code: 'en', primary_flg: true)
|
||||
expect(primary_tag_name_for(japanese, 'ja')).to have_attributes(
|
||||
name: 'same_name', tag_id: japanese.id, language_code: 'ja', primary_flg: true)
|
||||
expect(described_class.find_or_create_by_tag_name!(
|
||||
locale, 'same_name', category: :general)).to eq(english)
|
||||
expect(described_class.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'same_name', category: :general)).to eq(japanese)
|
||||
end
|
||||
|
||||
it 'preserves the V1 representative when looking up another primary language' do
|
||||
tag = create(:tag, primary_name: '日本語代表名')
|
||||
representative_id = tag.tag_name_id
|
||||
english = create(:tag_name, name: 'english_primary', tag:,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
|
||||
found = described_class.find_or_create_by_tag_name!(
|
||||
locale, english.name, category: :general)
|
||||
|
||||
expect(found).to eq(tag)
|
||||
expect(tag.reload.tag_name_id).to eq(representative_id)
|
||||
expect(english.reload.tag_id).to eq(tag.id)
|
||||
end
|
||||
|
||||
it 'normalises names using the supplied locale' do
|
||||
tags = described_class.normalise_tags!(
|
||||
locale, ['character:normalised_english'],
|
||||
with_tagme: false, with_no_deerjikist: false)
|
||||
|
||||
expect(tags.length).to eq(1)
|
||||
expect(primary_tag_name_for(tags.first, 'en')).to have_attributes(
|
||||
name: 'normalised_english', language_code: 'en', script_code: 'Latn',
|
||||
primary_flg: true, tag_id: tags.first.id)
|
||||
end
|
||||
end
|
||||
|
||||
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!(Locale.nipponese,
|
||||
' lookup_new ', category: :character)
|
||||
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
|
||||
|
||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('lookup_new')
|
||||
expect(tag.category).to eq('character')
|
||||
end
|
||||
|
||||
it 'reuses the owning tag for an alias without changing its category' do
|
||||
tag = create(:tag, category: :character)
|
||||
representative_id = tag.tag_name_id
|
||||
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
|
||||
expect(alias_name.reload).to have_attributes(
|
||||
tag_id: tag.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
found = nil
|
||||
|
||||
expect {
|
||||
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
||||
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')
|
||||
expect(tag.reload.tag_name_id).to eq(representative_id)
|
||||
end
|
||||
|
||||
it 'reuses the owning tag through another alias' do
|
||||
tag = create(:tag)
|
||||
primary = primary_tag_name_for(tag, 'ja')
|
||||
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
|
||||
other_alias = create(:tag_name, :alias, name: 'lookup_other_alias', tag:)
|
||||
|
||||
expect([alias_name.reload, other_alias.reload]).to all(have_attributes(
|
||||
tag_id: tag.id,
|
||||
primary_flg: false, language_code: 'ja'))
|
||||
|
||||
expect {
|
||||
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
||||
alias_name.name, category: :general)
|
||||
expect(found).to eq(tag)
|
||||
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
|
||||
|
||||
expect(primary_tag_name_for(tag.reload, 'ja')).to eq(primary)
|
||||
expect(tag).not_to be_valid
|
||||
expect(tag.errors[:deprecated_at]).to include('ニコタグは廃止できません.')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.merge_tags!' do
|
||||
let!(:target_tag) { create(:tag, category: :general) }
|
||||
let!(:source_tag) { create(:tag, category: :general) }
|
||||
let!(:source_tag_name) { primary_tag_name_for(source_tag, 'ja') }
|
||||
let!(:source_tag_name) { source_tag.tag_name }
|
||||
|
||||
let!(:post_record) do
|
||||
Post.create!(url: 'https://example.com/posts/1', title: 'test post')
|
||||
end
|
||||
|
||||
context 'with names in multiple languages' do
|
||||
include_context 'English locale'
|
||||
|
||||
it 'keeps the target primary and moves source names as aliases' do
|
||||
english_primary = create(:tag_name, tag: target_tag,
|
||||
name: 'target_english', language_code: 'en', script_code: 'Latn')
|
||||
source_primary = create(:tag_name, tag: source_tag,
|
||||
name: 'source_english', language_code: 'en', script_code: 'Latn')
|
||||
source_alias = create(:tag_name, :alias, tag: source_tag,
|
||||
name: 'source_english_alias', language_code: 'en', script_code: 'Latn')
|
||||
|
||||
described_class.merge_tags!(target_tag, [source_tag])
|
||||
|
||||
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(english_primary)
|
||||
expect([source_primary.reload, source_alias.reload]).to all(have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'en', primary_flg: false))
|
||||
end
|
||||
|
||||
it 'inherits a source primary when the target has no name in that language' do
|
||||
source_primary = create(:tag_name, tag: source_tag,
|
||||
name: 'inherited_english', language_code: 'en', script_code: 'Latn')
|
||||
source_alias = create(:tag_name, :alias, tag: source_tag,
|
||||
name: 'inherited_alias', language_code: 'en', script_code: 'Latn')
|
||||
|
||||
described_class.merge_tags!(target_tag, [source_tag])
|
||||
|
||||
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(source_primary)
|
||||
expect(source_primary.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'en', primary_flg: true)
|
||||
expect(source_alias.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'en', primary_flg: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merging a simple source tag' do
|
||||
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])
|
||||
|
||||
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(target_link).to be_present
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(source_pt.discarded_at).to be_present
|
||||
expect(source_pt.tag_id).to eq(source_tag.id)
|
||||
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(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -386,111 +206,39 @@ RSpec.describe Tag, type: :model do
|
||||
let!(:target_post_tag) { PostTag.create!(post: post_record, tag: target_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
|
||||
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)
|
||||
|
||||
it 'discards the source post_tag, keeps one active target post_tag, discards the source tag, and aliases the source tag_name' do
|
||||
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(target_links).to contain_exactly(target_post_tag)
|
||||
expect(PostTagSection.where(post: post_record, tag: source_tag)).to be_empty
|
||||
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
|
||||
expect(source_pt.discarded_at).to be_present
|
||||
expect(source_pt.tag_id).to eq(source_tag.id)
|
||||
expect(active.count).to eq(1)
|
||||
expect(active.first.id).to eq(target_post_tag.id)
|
||||
|
||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: 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(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'keeps source history and records the new target alias after deleting the source' do
|
||||
user = create_member_user!
|
||||
source_name = primary_tag_name_for(source_tag, 'ja').name
|
||||
source_alias = create(:tag_name, :alias, name: 'merge_alias', tag: source_tag)
|
||||
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.created_by_user).to eq(user)
|
||||
expect(versions.last.names).to include(
|
||||
hash_including('language_code' => 'ja', 'name' => source_name,
|
||||
'primary_flg' => true, 'auto_generated' => false),
|
||||
hash_including('language_code' => 'ja', 'name' => source_alias.name,
|
||||
'primary_flg' => false, 'auto_generated' => false))
|
||||
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'])
|
||||
target_aliases = target_versions.last.names.filter_map { |name|
|
||||
name['name'] if name['language_code'] == 'ja' && !name['primary_flg']
|
||||
}
|
||||
expect(target_aliases).to contain_exactly('merge_alias', source_name)
|
||||
end
|
||||
|
||||
it 'deletes source relationships while preserving unrelated relationships' do
|
||||
parent = create(:tag)
|
||||
child = create(:tag)
|
||||
nico_tag = create(:external_tag)
|
||||
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
|
||||
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
|
||||
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.unscoped.exists?(source_tag.id)).to be(false)
|
||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
||||
expect(target_link).to be_present
|
||||
expect(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
|
||||
expect(source_pt.discarded_at).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(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -512,17 +260,18 @@ RSpec.describe Tag, type: :model do
|
||||
)
|
||||
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])
|
||||
|
||||
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(target_link).to be_present
|
||||
expect(source_pt.discarded_at).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(source_tag_name.reload).to have_attributes(
|
||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
||||
expect(Tag.with_discarded.find(source_tag.id)).to be_discarded
|
||||
expect(target_tag.reload.post_count).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -539,30 +288,34 @@ RSpec.describe Tag, type: :model do
|
||||
message: 'init')
|
||||
end
|
||||
|
||||
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do
|
||||
earlier_source = create(:tag)
|
||||
earlier_name = primary_tag_name_for(earlier_source, 'ja')
|
||||
source_section = create(:post_tag_section, post: post_record,
|
||||
tag: source_tag,
|
||||
begin_ms: 1000, end_ms: 2000)
|
||||
|
||||
it 'rolls back the transaction' do
|
||||
expect {
|
||||
described_class.merge_tags!(target_tag, [earlier_source, source_tag])
|
||||
described_class.merge_tags!(target_tag, [source_tag])
|
||||
}.to raise_error(ActiveRecord::RecordInvalid)
|
||||
|
||||
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
|
||||
expect(earlier_name.reload).to have_attributes(
|
||||
tag_id: earlier_source.id, language_code: 'ja', primary_flg: true)
|
||||
expect(TagVersion.where(tag_id: [earlier_source.id, source_tag.id, target_tag.id]))
|
||||
.to be_empty
|
||||
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).to have_attributes(
|
||||
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
|
||||
expect(Tag.with_discarded.find(source_tag.id)).not_to be_discarded
|
||||
expect(TagName.with_discarded.find(source_tag_name.id)).not_to be_discarded
|
||||
expect(PostTag.kept.find(source_post_tag.id).tag_id).to eq(source_tag.id)
|
||||
expect(PostTag.kept.find_by(post_id: post_record.id, tag_id: target_tag.id)).to be_nil
|
||||
expect(source_tag_name.reload.canonical_id).to be_nil
|
||||
expect(target_tag.reload.post_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merging a nico source tag' do
|
||||
let!(:target_tag) { create(:tag, category: :nico, name: 'nico:foo') }
|
||||
let!(:source_tag) { create(:tag, category: :nico, name: 'nico:bar') }
|
||||
let!(:source_tag_name_id) { source_tag.tag_name_id }
|
||||
|
||||
it 'discards the source tag_name instead of aliasing it' do
|
||||
described_class.merge_tags!(target_tag, [source_tag])
|
||||
|
||||
discarded_source_tag = Tag.with_discarded.find(source_tag.id)
|
||||
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)
|
||||
end
|
||||
end
|
||||
@@ -580,7 +333,6 @@ RSpec.describe Tag, type: :model do
|
||||
url: post.url,
|
||||
thumbnail_base: post.thumbnail_base,
|
||||
tags: snapshot_tags(post),
|
||||
tags_json: post.snapshot_tags_json,
|
||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||
original_created_from: post.original_created_from,
|
||||
original_created_before: post.original_created_before,
|
||||
@@ -612,15 +364,12 @@ RSpec.describe Tag, type: :model do
|
||||
expect(latest.event_type).to eq('update')
|
||||
expect(latest.created_by_user).to be_nil
|
||||
expect(latest.tags).to eq(snapshot_tags(post_record.reload))
|
||||
expect(latest.tags_json.map { |item| item.fetch('tag_id') }).to eq([target_tag.id])
|
||||
expect(affected_versions.first.tags_json.map { |item| item.fetch('tag_id') })
|
||||
.to eq([source_tag.id])
|
||||
|
||||
expect(unaffected_post.reload.post_versions.count).to eq(1)
|
||||
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
|
||||
Post.create!(url: 'https://example.com/posts/3', title: 'another post')
|
||||
end
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe VersionRecord, type: :model do
|
||||
let!(:tag) { create(:tag, primary_name: 'version_record_tag') }
|
||||
let!(:nico_tag) { create(:external_tag, name: 'version_record_tag') }
|
||||
let!(:tag) { create(:tag, name: 'version_record_tag') }
|
||||
let!(:nico_tag) { create(:tag, :nico, name: 'nico:version_record_tag') }
|
||||
|
||||
it 'makes TagVersion read only after create' do
|
||||
version = TagVersion.create!(
|
||||
tag: tag,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
name: primary_tag_name_for(tag, 'ja').name,
|
||||
name: tag.name,
|
||||
category: tag.category,
|
||||
aliases: '',
|
||||
names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name,
|
||||
script_code: 'Jpan', primary_flg: true }],
|
||||
parent_tag_ids: '',
|
||||
created_at: Time.current,
|
||||
created_by_user: nil
|
||||
@@ -29,11 +27,9 @@ RSpec.describe VersionRecord, type: :model do
|
||||
tag: tag,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
name: primary_tag_name_for(tag, 'ja').name,
|
||||
name: tag.name,
|
||||
category: tag.category,
|
||||
aliases: '',
|
||||
names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name,
|
||||
script_code: 'Jpan', primary_flg: true }],
|
||||
parent_tag_ids: '',
|
||||
created_at: Time.current,
|
||||
created_by_user: nil
|
||||
@@ -46,10 +42,10 @@ RSpec.describe VersionRecord, type: :model do
|
||||
|
||||
it 'makes NicoTagVersion read only after create' do
|
||||
version = NicoTagVersion.create!(
|
||||
external_tag: nico_tag,
|
||||
tag: nico_tag,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
name: "nico:#{ nico_tag.name }",
|
||||
name: nico_tag.name,
|
||||
linked_tags: '',
|
||||
created_at: Time.current,
|
||||
created_by_user: nil
|
||||
@@ -62,10 +58,10 @@ RSpec.describe VersionRecord, type: :model do
|
||||
|
||||
it 'prevents NicoTagVersion destroy' do
|
||||
version = NicoTagVersion.create!(
|
||||
external_tag: nico_tag,
|
||||
tag: nico_tag,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
name: "nico:#{ nico_tag.name }",
|
||||
name: nico_tag.name,
|
||||
linked_tags: '',
|
||||
created_at: Time.current,
|
||||
created_by_user: nil
|
||||
|
||||
@@ -30,7 +30,7 @@ RSpec.describe 'Deerjikists API', type: :request do
|
||||
|
||||
expect(json['tag']).to be_a(Hash)
|
||||
expect(json['tag']['id']).to eq(tag1.id)
|
||||
expect(json['tag']['name']).to eq(primary_tag_name_for(tag1, 'ja').name)
|
||||
expect(json['tag']['name']).to eq(tag1.name)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ RSpec.describe 'error responses', type: :request do
|
||||
|
||||
it 'returns a stable field-error payload for unprocessable requests' do
|
||||
member = create(:user, :member)
|
||||
tag = create(:tag, category: :general, primary_name: 'error_response_tag')
|
||||
tag = create(:tag, :general, name: 'error_response_tag')
|
||||
sign_in_as(member)
|
||||
|
||||
patch "/tags/#{ tag.id }", params: { category: 'nico' }
|
||||
|
||||
@@ -898,22 +898,23 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
||||
|
||||
describe 'GET /gekanator/questions' do
|
||||
it 'omits questions for deprecated tags' do
|
||||
active_tag = create(:tag, primary_name: 'active_question_tag', category: :general)
|
||||
deprecated_tag = create(:tag,
|
||||
primary_name: 'deprecated_question_tag',
|
||||
active_tag = Tag.create!(name: 'active_question_tag', category: :general)
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'deprecated_question_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current)
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
|
||||
[active_tag, deprecated_tag].each do |question_tag|
|
||||
GekanatorQuestion.create!(
|
||||
text: "#{ primary_tag_name_for(question_tag, 'ja').name }?",
|
||||
text: "#{ question_tag.name }?",
|
||||
kind: 'tag',
|
||||
source: 'admin_curated',
|
||||
status: 'accepted',
|
||||
priority_weight: 1.0,
|
||||
condition: {
|
||||
type: 'tag',
|
||||
key: "#{ question_tag.category }:#{ primary_tag_name_for(question_tag, 'ja').name }"
|
||||
key: "#{ question_tag.category }:#{ question_tag.name }"
|
||||
},
|
||||
created_by: admin
|
||||
)
|
||||
|
||||
@@ -4,11 +4,12 @@ require 'rails_helper'
|
||||
RSpec.describe 'Gekanator posts API', type: :request do
|
||||
describe 'GET /gekanator/posts' do
|
||||
it 'omits deprecated tags and returns the stored similarity cosine' do
|
||||
active_tag = create(:tag, primary_name: 'active tag', category: :general)
|
||||
deprecated_tag = create(:tag,
|
||||
primary_name: 'deprecated tag',
|
||||
active_tag = Tag.create!(name: 'active tag', category: :general)
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'deprecated tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current)
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
post_record = Post.create!(title: 'source', url: 'https://example.com/source')
|
||||
target_post = Post.create!(title: 'target', url: 'https://example.com/target')
|
||||
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Locale propagation on write paths', type: :request do
|
||||
include_context 'English locale'
|
||||
|
||||
let(:member) { create(:user, :member) }
|
||||
|
||||
before do
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
def expect_localised_tag(name, expected_locale)
|
||||
tag_name = TagName.find_by!(name:, language_code: expected_locale.language_code)
|
||||
expect(tag_name).to have_attributes(
|
||||
script_code: expected_locale.script_code, primary_flg: true)
|
||||
expect(tag_name.tag_id).to be_present
|
||||
tag_name.tag
|
||||
end
|
||||
|
||||
['en', nil].each do |requested_locale|
|
||||
context "with locale #{ requested_locale.inspect }" do
|
||||
let(:locale_params) { requested_locale ? { locale: requested_locale } : { } }
|
||||
let(:expected_locale) { requested_locale ? locale : Locale.nipponese }
|
||||
|
||||
it 'creates post tags in the requested language or Japanese fallback' do
|
||||
post '/posts', params: locale_params.merge(
|
||||
title: 'Locale post', url: 'https://example.com/locale-post',
|
||||
tags: 'locale_post_tag', parent_post_ids: '')
|
||||
|
||||
expect(response).to have_http_status(:created), response.body
|
||||
tag = expect_localised_tag('locale_post_tag', expected_locale)
|
||||
expect(Post.find(json.fetch('id')).tags).to include(tag)
|
||||
end
|
||||
|
||||
it 'updates post tags in the requested language or Japanese fallback' do
|
||||
record = create(:post)
|
||||
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
|
||||
|
||||
put "/posts/#{ record.id }", params: locale_params.merge(
|
||||
title: record.title, tags: 'locale_updated_tag', parent_post_ids: '',
|
||||
base_version_no: record.reload.version_no)
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
tag = expect_localised_tag('locale_updated_tag', expected_locale)
|
||||
expect(record.reload.tags).to include(tag)
|
||||
end
|
||||
|
||||
it 'passes the resolved locale to bulk creation' do
|
||||
# Worker propagation is exercised in post_bulk_creator_spec.
|
||||
expect_any_instance_of(PostBulkCreator).to receive(:run)
|
||||
.with(expected_locale).and_return(results: [])
|
||||
manifest = [{ title: 'Bulk locale', url: 'https://example.com/bulk-locale',
|
||||
tags: 'bulk_locale_tag', parent_post_ids: '' }]
|
||||
|
||||
post '/posts/bulk', params: locale_params.merge(posts: JSON.generate(manifest)),
|
||||
headers: { 'CONTENT_TYPE' => 'multipart/form-data' }
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
end
|
||||
|
||||
it 'creates materials with tags in the resolved locale' do
|
||||
post '/materials', params: locale_params.merge(
|
||||
tag: 'locale_material', url: 'https://example.com/material')
|
||||
|
||||
expect(response).to have_http_status(:created), response.body
|
||||
tag = expect_localised_tag('locale_material', expected_locale)
|
||||
expect(Material.find(json.fetch('id')).tag).to eq(tag)
|
||||
end
|
||||
|
||||
it 'updates materials with tags in the resolved locale' do
|
||||
tag = create(:tag, category: :material)
|
||||
material = Material.create!(tag:, url: 'https://example.com/material')
|
||||
|
||||
put "/materials/#{ material.id }", params: locale_params.merge(
|
||||
tag: 'locale_material_updated', url: material.url)
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
resolved_tag = expect_localised_tag('locale_material_updated', expected_locale)
|
||||
expect(material.reload.tag).to eq(resolved_tag)
|
||||
end
|
||||
|
||||
it 'links Nico external tags to internal tags in the resolved locale' do
|
||||
external = create(:external_tag)
|
||||
NicoTagVersionRecorder.record!(
|
||||
external_tag: external, event_type: :create, created_by_user: member)
|
||||
|
||||
put "/tags/nico/#{ external.id }", params: locale_params.merge(tags: 'locale_link')
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
tag = expect_localised_tag('locale_link', expected_locale)
|
||||
expect(external.reload.linked_tags).to contain_exactly(tag)
|
||||
end
|
||||
|
||||
it 'creates parent tags in the resolved locale' do
|
||||
tag = create(:tag)
|
||||
|
||||
put "/tags/#{ tag.id }", params: locale_params.merge(
|
||||
name: primary_tag_name_for(tag, 'ja').name, category: tag.category, deprecated: false,
|
||||
aliases: '', parent_tags: 'locale_parent')
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
parent = expect_localised_tag('locale_parent', expected_locale)
|
||||
expect(TagImplication.where(tag:).pluck(:parent_tag_id)).to eq([parent.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with existing names in both languages' do
|
||||
let!(:english_name) do
|
||||
create(:tag_name, name: 'shared_name', language_code: 'en', script_code: 'Latn')
|
||||
end
|
||||
let!(:english_tag) { create(:tag, primary_tag_name: english_name, category: :material) }
|
||||
let!(:japanese_tag) do
|
||||
create(:tag, primary_name: 'temporary_japanese', category: :general).tap do |tag|
|
||||
# Isolate lookup from the separately tested uniqueness validation.
|
||||
primary_tag_name_for(tag, 'ja').update_columns(name: 'shared_name')
|
||||
end
|
||||
end
|
||||
|
||||
it 'plans and creates the post using the English identity and category' do
|
||||
post '/posts', params: {
|
||||
locale: 'en', title: 'English identity',
|
||||
url: 'https://example.com/english-identity', tags: 'shared_name', parent_post_ids: '' }
|
||||
|
||||
expect(response).to have_http_status(:created), response.body
|
||||
record = Post.find(json.fetch('id'))
|
||||
expect(record.tags).to include(english_tag)
|
||||
expect(record.tags).not_to include(japanese_tag)
|
||||
expect(english_tag.reload.category).to eq('material')
|
||||
expect(japanese_tag.reload.category).to eq('general')
|
||||
end
|
||||
|
||||
it 'updates the post using the English identity' do
|
||||
record = create(:post)
|
||||
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
|
||||
|
||||
put "/posts/#{ record.id }", params: {
|
||||
locale: 'en', title: record.title, tags: 'shared_name', parent_post_ids: '',
|
||||
base_version_no: record.reload.version_no }
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
expect(record.reload.tags).to include(english_tag)
|
||||
expect(record.tags).not_to include(japanese_tag)
|
||||
end
|
||||
|
||||
it 'resolves the English material tag' do
|
||||
post '/materials', params: {
|
||||
locale: 'en', tag: 'shared_name', url: 'https://example.com/english-material' }
|
||||
|
||||
expect(response).to have_http_status(:created), response.body
|
||||
expect(Material.find(json.fetch('id')).tag).to eq(english_tag)
|
||||
expect(Material.where(tag: japanese_tag)).to be_empty
|
||||
end
|
||||
|
||||
it 'resolves the English parent tag' do
|
||||
child = create(:tag)
|
||||
|
||||
put "/tags/#{ child.id }", params: {
|
||||
locale: 'en', name: primary_tag_name_for(child, 'ja').name,
|
||||
category: child.category, deprecated: false,
|
||||
aliases: '', parent_tags: 'shared_name' }
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
expect(TagImplication.where(tag: child).pluck(:parent_tag_id)).to eq([english_tag.id])
|
||||
end
|
||||
|
||||
it 'resolves the English internal tag for a Nico link' do
|
||||
external = create(:external_tag)
|
||||
NicoTagVersionRecorder.record!(
|
||||
external_tag: external, event_type: :create, created_by_user: member)
|
||||
|
||||
put "/tags/nico/#{ external.id }", params: { locale: 'en', tags: 'shared_name' }
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
expect(external.reload.linked_tags).to contain_exactly(english_tag)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when renaming or updating aliases in English' do
|
||||
let!(:english_name) do
|
||||
create(:tag_name, name: 'english_original', language_code: 'en', script_code: 'Latn')
|
||||
end
|
||||
let!(:tag) { create(:tag, primary_tag_name: english_name) }
|
||||
|
||||
it 'allows a rename to a name already used in Japanese' do
|
||||
japanese = create(:tag, primary_name: 'rename_target')
|
||||
|
||||
patch "/tags/#{ tag.id }", params: { locale: 'en', name: 'rename_target' }
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
expect(primary_tag_name_for(tag.reload, 'en')).to have_attributes(
|
||||
name: 'rename_target', language_code: 'en', tag_id: tag.id)
|
||||
expect(primary_tag_name_for(japanese.reload, 'ja')).to have_attributes(
|
||||
name: 'rename_target', language_code: 'ja', tag_id: japanese.id)
|
||||
end
|
||||
|
||||
it 'creates aliases in the requested language' do
|
||||
put "/tags/#{ tag.id }", params: {
|
||||
locale: 'en', name: english_name.name,
|
||||
category: tag.category, deprecated: false,
|
||||
aliases: 'english_alias', parent_tags: '' }
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
expect(TagName.find_by!(language_code: 'en', name: 'english_alias'))
|
||||
.to have_attributes(
|
||||
script_code: 'Latn',
|
||||
tag_id: tag.id, primary_flg: false, language_code: 'en')
|
||||
primary = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
|
||||
expect(primary).to have_attributes(name: 'english_original', script_code: 'Latn')
|
||||
end
|
||||
|
||||
it 'does not take an alias from another language' do
|
||||
japanese = create(:tag, primary_name: 'japanese_owner')
|
||||
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: japanese)
|
||||
expect(japanese_alias.reload).to have_attributes(
|
||||
tag_id: japanese.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
|
||||
put "/tags/#{ tag.id }", params: {
|
||||
locale: 'en', name: english_name.name,
|
||||
category: tag.category, deprecated: false,
|
||||
aliases: 'shared_alias', parent_tags: '' }
|
||||
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
expect(japanese_alias.reload).to have_attributes(
|
||||
tag_id: japanese.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
expect(TagName.find_by!(language_code: 'en', name: 'shared_alias'))
|
||||
.to have_attributes(
|
||||
tag_id: tag.id, script_code: 'Latn',
|
||||
primary_flg: false, language_code: 'en')
|
||||
expect(TagName.where(name: 'shared_alias').pluck(:language_code, :tag_id))
|
||||
.to contain_exactly(['ja', japanese.id], ['en', tag.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -26,10 +26,10 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
describe 'GET /materials' do
|
||||
let!(:tag_a) do
|
||||
create(:tag, primary_name: 'material_index_a', category: :material)
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_index_a'), category: :material)
|
||||
end
|
||||
let!(:tag_b) do
|
||||
create(:tag, primary_name: 'material_index_b', category: :material)
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_index_b'), category: :material)
|
||||
end
|
||||
|
||||
let!(:material_a) do
|
||||
@@ -116,20 +116,20 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
it 'filters by descendant tags and returns stable parent tag groups' do
|
||||
root =
|
||||
create(:tag, primary_name: 'material_scope_root',
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_scope_root'),
|
||||
category: :material)
|
||||
child_b =
|
||||
create(:tag, primary_name: 'material_scope_b',
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_scope_b'),
|
||||
category: :material)
|
||||
child_a =
|
||||
create(:tag, primary_name: 'material_scope_a',
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_scope_a'),
|
||||
category: :material)
|
||||
deprecated =
|
||||
create(:tag, primary_name: 'material_scope_old',
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_scope_old'),
|
||||
category: :material,
|
||||
deprecated_at: Time.current)
|
||||
grandchild =
|
||||
create(:tag, primary_name: 'material_scope_grandchild',
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_scope_grandchild'),
|
||||
category: :material)
|
||||
root_material =
|
||||
build_material(tag: root, user: member_user,
|
||||
@@ -179,7 +179,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
describe 'GET /materials/:id' do
|
||||
let!(:tag) do
|
||||
create(:tag, primary_name: 'material_show', category: :material)
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material)
|
||||
end
|
||||
let!(:material) do
|
||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
|
||||
@@ -269,7 +269,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
material = Material.order(:id).last
|
||||
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_new')
|
||||
expect(material.tag.name).to eq('material_create_new')
|
||||
expect(material.tag.category).to eq('material')
|
||||
expect(material.created_by_user).to eq(member_user)
|
||||
expect(material.updated_by_user).to eq(member_user)
|
||||
@@ -297,8 +297,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
tag = TagName.find_by!(name: 'material_create_versioned_tag',
|
||||
language_code: 'ja', primary_flg: true).tag
|
||||
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_create_versioned_tag' })
|
||||
version = tag.tag_versions.order(:version_no).last
|
||||
|
||||
expect(version.event_type).to eq('create')
|
||||
@@ -324,8 +323,8 @@ RSpec.describe 'Materials API', type: :request do
|
||||
end
|
||||
|
||||
it 'returns 422 when the existing tag is not material/character' do
|
||||
general_tag_name = create(:tag_name, name: 'material_create_general_tag')
|
||||
create(:tag, primary_tag_name: general_tag_name, category: :general)
|
||||
general_tag_name = TagName.create!(name: 'material_create_general_tag')
|
||||
Tag.create!(tag_name: general_tag_name, category: :general)
|
||||
|
||||
post '/materials', params: {
|
||||
tag: 'material_create_general_tag',
|
||||
@@ -346,7 +345,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
material = Material.order(:id).last
|
||||
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_url_only')
|
||||
expect(material.tag.name).to eq('material_create_url_only')
|
||||
expect(material.url).to eq('https://example.com/material-source')
|
||||
expect(material.file.attached?).to be(false)
|
||||
end
|
||||
@@ -385,7 +384,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
describe 'PUT /materials/:id' do
|
||||
let!(:tag) do
|
||||
create(:tag, primary_name: 'material_update_old', category: :material)
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_update_old'), category: :material)
|
||||
end
|
||||
let!(:material) do
|
||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
|
||||
@@ -464,7 +463,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
material.reload
|
||||
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_new')
|
||||
expect(material.tag.name).to eq('material_update_new')
|
||||
expect(material.tag.category).to eq('material')
|
||||
expect(material.url).to eq('https://example.com/updated-source')
|
||||
expect(material.updated_by_user).to eq(member_user)
|
||||
@@ -498,8 +497,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
tag = TagName.find_by!(name: 'material_update_versioned_tag',
|
||||
language_code: 'ja', primary_flg: true).tag
|
||||
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_update_versioned_tag' })
|
||||
version = tag.tag_versions.order(:version_no).last
|
||||
|
||||
expect(version.event_type).to eq('create')
|
||||
@@ -510,7 +508,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
it 'backfills a create tag_version for an existing material tag without history' do
|
||||
existing_tag =
|
||||
create(:tag, primary_name: 'material_update_existing_no_history',
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_update_existing_no_history'),
|
||||
category: :material)
|
||||
|
||||
expect(existing_tag.tag_versions).to be_empty
|
||||
@@ -533,7 +531,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
it 'backfills a create tag_version for an existing character tag without history' do
|
||||
existing_tag =
|
||||
create(:tag, primary_name: 'material_update_character_no_history',
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_update_character_no_history'),
|
||||
category: :character)
|
||||
|
||||
expect(existing_tag.tag_versions).to be_empty
|
||||
@@ -565,7 +563,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
material.reload
|
||||
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_remove_file')
|
||||
expect(material.tag.name).to eq('material_update_remove_file')
|
||||
expect(material.url).to eq('https://example.com/updated-source')
|
||||
expect(material.updated_by_user).to eq(member_user)
|
||||
expect(material.file.attached?).to be(false)
|
||||
@@ -653,8 +651,8 @@ RSpec.describe 'Materials API', type: :request do
|
||||
end
|
||||
|
||||
describe 'GET /materials/download.zip' do
|
||||
let!(:tag_a) { create(:tag, primary_name: 'zip_a', category: :material) }
|
||||
let!(:tag_b) { create(:tag, primary_name: 'zip_b', category: :material) }
|
||||
let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'zip_a'), category: :material) }
|
||||
let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'zip_b'), category: :material) }
|
||||
let!(:material_a) do
|
||||
build_material(tag: tag_a, user: member_user,
|
||||
file: dummy_upload(filename: 'a.png', body: 'zip-a'))
|
||||
@@ -703,7 +701,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
describe 'GET /materials/versions' do
|
||||
let!(:tag) do
|
||||
create(:tag, primary_name: 'material_history', category: :material)
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_history'), category: :material)
|
||||
end
|
||||
let!(:material) do
|
||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'history.png'))
|
||||
@@ -778,7 +776,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
|
||||
describe 'DELETE /materials/:id' do
|
||||
let!(:tag) do
|
||||
create(:tag, primary_name: 'material_destroy', category: :material)
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material)
|
||||
end
|
||||
let!(:material) do
|
||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
|
||||
|
||||
@@ -3,34 +3,10 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe 'NicoTags', type: :request do
|
||||
describe 'GET /tags/nico' do
|
||||
it 'returns the legacy Tag-compatible external fields' do
|
||||
external = create(:external_tag, name: 'legacy_external', post_count: 3)
|
||||
|
||||
get '/tags/nico', params: { name: 'legacy_external' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(1)
|
||||
expect(json.fetch('tags')).to contain_exactly(
|
||||
a_hash_including(
|
||||
'id' => external.id,
|
||||
'name' => 'nico:legacy_external',
|
||||
'category' => 'nico',
|
||||
'post_count' => 3,
|
||||
'created_at' => external.created_at.as_json,
|
||||
'updated_at' => external.created_at.as_json,
|
||||
'deprecated_at' => nil,
|
||||
'aliases' => [],
|
||||
'parents' => [],
|
||||
'has_wiki' => false,
|
||||
'material_id' => nil,
|
||||
'has_deerjikists' => false,
|
||||
'linked_tags' => []))
|
||||
end
|
||||
|
||||
it 'returns paginated tags and total count' do
|
||||
3.times { |i| create(:external_tag, name: "pagination_#{ i }") }
|
||||
create_list(:tag, 3, :nico)
|
||||
|
||||
get '/tags/nico', params: { page: 2, limit: 2, name: 'pagination_' }
|
||||
get '/tags/nico', params: { page: 2, limit: 2 }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json['tags'].size).to eq(1)
|
||||
@@ -38,16 +14,16 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
end
|
||||
|
||||
it 'filters by nico tag name, linked tag name, and link status' do
|
||||
linked = create(:external_tag)
|
||||
linked.update!(name: 'search_linked')
|
||||
unlinked = create(:external_tag)
|
||||
unlinked.update!(name: 'search_unlinked')
|
||||
other = create(:external_tag)
|
||||
other.update!(name: 'other')
|
||||
destination = create(:tag, category: :general)
|
||||
primary_tag_name_for(destination, 'ja').update!(name: 'destination_search')
|
||||
linked = create(:tag, :nico)
|
||||
linked.tag_name.update!(name: 'nico:search_linked')
|
||||
unlinked = create(:tag, :nico)
|
||||
unlinked.tag_name.update!(name: 'nico:search_unlinked')
|
||||
other = create(:tag, :nico)
|
||||
other.tag_name.update!(name: 'nico:other')
|
||||
destination = create(:tag, :general)
|
||||
destination.tag_name.update!(name: 'destination_search')
|
||||
NicoTagRelation.create!(nico_tag: linked, tag: destination)
|
||||
NicoTagRelation.create!(nico_tag: other, tag: create(:tag, category: :general))
|
||||
NicoTagRelation.create!(nico_tag: other, tag: create(:tag, :general))
|
||||
|
||||
get '/tags/nico', params: {
|
||||
name: 'search_',
|
||||
@@ -65,77 +41,37 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
expect(json.fetch('tags').map { |tag| tag['id'] }).to eq([unlinked.id])
|
||||
end
|
||||
|
||||
it 'filters by the qualified legacy name as well as the raw name' do
|
||||
external = create(:external_tag, name: 'qualified_filter')
|
||||
create(:external_tag, name: 'unrelated_filter')
|
||||
|
||||
['qualified_filter', 'nico:qualified_filter'].each do |name|
|
||||
get '/tags/nico', params: { name: }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(1)
|
||||
expect(json.fetch('tags')).to contain_exactly(
|
||||
a_hash_including('id' => external.id, 'name' => 'nico:qualified_filter'))
|
||||
end
|
||||
end
|
||||
|
||||
it 'sorts by name and timestamps' do
|
||||
older = create(:external_tag)
|
||||
older.update!(name: 'ordered_a')
|
||||
older = create(:tag, :nico)
|
||||
older.tag_name.update!(name: 'nico:a')
|
||||
older.update_columns(created_at: 2.days.ago)
|
||||
newer = create(:external_tag)
|
||||
newer.update!(name: 'ordered_b')
|
||||
newer = create(:tag, :nico)
|
||||
newer.tag_name.update!(name: 'nico:b')
|
||||
newer.update_columns(created_at: 1.day.ago)
|
||||
older_post_tag =
|
||||
PostExternalTag.create!(post: create(:post), external_tag: older)
|
||||
PostTag.create!(post: Post.create!(url: 'https://example.com/nico-older'), tag: older)
|
||||
older_post_tag.update_columns(created_at: 1.hour.ago)
|
||||
newer_post_tag =
|
||||
PostExternalTag.create!(post: create(:post), external_tag: newer)
|
||||
PostTag.create!(post: Post.create!(url: 'https://example.com/nico-newer'), tag: newer)
|
||||
newer_post_tag.update_columns(created_at: 2.hours.ago)
|
||||
|
||||
get '/tags/nico', params: { order: 'name:desc', name: 'ordered_' }
|
||||
get '/tags/nico', params: { order: 'name:desc' }
|
||||
expect(json.fetch('tags').map { |tag| tag['id'] }).to eq([newer.id, older.id])
|
||||
|
||||
get '/tags/nico', params: { order: 'created_at:asc', name: 'ordered_' }
|
||||
get '/tags/nico', params: { order: 'created_at:asc' }
|
||||
expect(json.fetch('tags').map { |tag| tag['id'] }).to eq([older.id, newer.id])
|
||||
|
||||
get '/tags/nico', params: { order: 'updated_at:desc', name: 'ordered_' }
|
||||
get '/tags/nico', params: { order: 'updated_at:desc' }
|
||||
expect(json.fetch('tags').map { |tag| tag['id'] }).to eq([older.id, newer.id])
|
||||
expect(Time.zone.parse(json.fetch('tags').first.fetch('recent_post_tag_created_at')))
|
||||
.to be_within(1.second).of(older_post_tag.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /tags/nico/:id' do
|
||||
it 'returns the external tag even when an internal tag has the same id' do
|
||||
internal = create(:tag)
|
||||
external = create(
|
||||
:external_tag,
|
||||
id: internal.id,
|
||||
name: 'external_detail')
|
||||
|
||||
get "/tags/nico/#{ external.id }"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include(
|
||||
'id' => external.id,
|
||||
'name' => 'nico:external_detail',
|
||||
'category' => 'nico')
|
||||
end
|
||||
|
||||
it 'returns 404 when the external tag does not exist' do
|
||||
internal = create(:tag)
|
||||
|
||||
get "/tags/nico/#{ internal.id }"
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /tags/nico/:id' do
|
||||
let(:member) { create(:user, :member) }
|
||||
let(:admin) { create(:user, :admin) }
|
||||
let(:nico_tag) { create(:external_tag) }
|
||||
let(:nico_tag) { create(:tag, :nico) }
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
@@ -149,31 +85,30 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it '404 when only an internal tag exists for the target id' do
|
||||
it '400 when target is not nico category' do
|
||||
sign_in_as(member)
|
||||
non_nico = create(:tag, category: :general)
|
||||
expect(ExternalTag.exists?(non_nico.id)).to be(false)
|
||||
non_nico = create(:tag, :general)
|
||||
patch "/tags/nico/#{non_nico.id}", params: { tags: 'a b' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it '200 and updates linked tags while recording tag versions' do
|
||||
sign_in_as(admin)
|
||||
|
||||
nico_tag = create(:external_tag, name: 'nico_tags_spec_source')
|
||||
nico_tag_name = TagName.create!(name: 'nico:nico_tags_spec_source')
|
||||
nico_tag = Tag.create!(tag_name: nico_tag_name, category: :nico)
|
||||
|
||||
linked_a_name = create(:tag_name, name: 'nico_linked_a')
|
||||
linked_a = create(:tag, primary_tag_name: linked_a_name, category: :general)
|
||||
linked_a_name = TagName.create!(name: 'nico_linked_a')
|
||||
linked_a = Tag.create!(tag_name: linked_a_name, category: :general)
|
||||
|
||||
linked_b_name = create(:tag_name, name: 'nico_linked_b')
|
||||
linked_b = create(:tag, primary_tag_name: linked_b_name, category: :general)
|
||||
linked_b_name = TagName.create!(name: 'nico_linked_b')
|
||||
linked_b = Tag.create!(tag_name: linked_b_name, category: :general)
|
||||
|
||||
NicoTagVersionRecorder.record!(external_tag: nico_tag,
|
||||
event_type: :create, created_by_user: admin)
|
||||
TagVersioning.ensure_snapshot!(nico_tag, created_by_user: admin)
|
||||
|
||||
expect {
|
||||
patch "/tags/nico/#{nico_tag.id}", params: {
|
||||
tags: " #{ linked_a_name.name }\n#{ linked_b_name.name } "
|
||||
tags: " #{linked_a.name}\n#{linked_b.name} "
|
||||
}
|
||||
}.to change(TagVersion, :count).by(2)
|
||||
.and change(NicoTagVersion, :count).by(1)
|
||||
@@ -191,26 +126,26 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
expect(versions.map(&:event_type)).to eq(['create', 'update'])
|
||||
expect(versions.last.linked_tags.split).to match_array([
|
||||
'nico_linked_a',
|
||||
'nico_linked_b'])
|
||||
'nico_linked_b'
|
||||
])
|
||||
expect(versions.last.created_by_user_id).to eq(admin.id)
|
||||
end
|
||||
|
||||
it 'clears existing links and records the empty mapping for a member' do
|
||||
it 'returns 422 when linked tag normalises to nico tag' do
|
||||
sign_in_as(member)
|
||||
linked = create(:tag)
|
||||
NicoTagRelation.insert_all!([{ nico_tag_id: nico_tag.id, tag_id: linked.id }])
|
||||
NicoTagVersionRecorder.record!(external_tag: nico_tag,
|
||||
event_type: :create, created_by_user: member)
|
||||
|
||||
other_nico = create(:tag, :nico, name: 'nico:linked_ng')
|
||||
TagName.create!(name: 'linked_ng_alias', canonical: other_nico.tag_name)
|
||||
|
||||
TagVersioning.ensure_snapshot!(nico_tag, created_by_user: member)
|
||||
|
||||
expect {
|
||||
patch "/tags/nico/#{ nico_tag.id }", params: { tags: '' }
|
||||
}.to change(NicoTagVersion, :count).by(1)
|
||||
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'linked_ng_alias' }
|
||||
}.not_to change(NicoTagVersion, :count)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to eq([])
|
||||
expect(nico_tag.reload.linked_tags).to be_empty
|
||||
expect(nico_tag.nico_tag_versions.order(:version_no).last)
|
||||
.to have_attributes(linked_tags: '', created_by_user: member)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tags' => ['ニコニコ・タグ同士は連携できません.'])
|
||||
end
|
||||
|
||||
it 'returns the tags field error when a nico tag is specified directly' do
|
||||
|
||||
+195
-479
@@ -17,7 +17,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
|
||||
def create_nico_tag!(name)
|
||||
ExternalTag.find_or_create_by!(platform: :nico, name: name.delete_prefix('nico:'))
|
||||
Tag.find_or_create_by_tag_name!(name, category: :nico)
|
||||
end
|
||||
|
||||
def dummy_upload
|
||||
@@ -55,7 +55,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail_base: post.thumbnail_base,
|
||||
video_ms: post.video_ms,
|
||||
tags: post.snapshot_tag_names.join(' '),
|
||||
tags_json: post.snapshot_tags_json,
|
||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||
original_created_from: post.original_created_from,
|
||||
original_created_before: post.original_created_before,
|
||||
@@ -93,28 +92,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
count
|
||||
end
|
||||
|
||||
def expect_external_tag_json tag_json, external_tag
|
||||
external_tag.reload
|
||||
|
||||
expect(tag_json).to include(
|
||||
'id' => external_tag.id,
|
||||
'name' => "#{ external_tag.platform }:#{ external_tag.name }",
|
||||
'category' => 'nico',
|
||||
'created_at' => external_tag.created_at.as_json,
|
||||
'updated_at' => external_tag.created_at.as_json,
|
||||
'deprecated_at' => nil,
|
||||
'aliases' => [],
|
||||
'parents' => [],
|
||||
'post_count' => external_tag.post_count,
|
||||
'has_wiki' => false,
|
||||
'material_id' => nil,
|
||||
'has_deerjikists' => false,
|
||||
'children' => [],
|
||||
'sections' => [])
|
||||
end
|
||||
|
||||
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
|
||||
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
|
||||
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
|
||||
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
|
||||
|
||||
let!(:post_record) do
|
||||
Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
|
||||
@@ -125,11 +104,11 @@ RSpec.describe 'Posts API', type: :request do
|
||||
describe 'GET /posts' do
|
||||
let!(:user) { create_member_user! }
|
||||
|
||||
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
|
||||
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
|
||||
let!(:tag_name2) { create(:tag_name, name: 'unko') }
|
||||
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :deerjikist) }
|
||||
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
|
||||
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
|
||||
let!(:tag) { Tag.create!(tag_name:, category: :general) }
|
||||
let!(:tag_name2) { TagName.create!(name: 'unko') }
|
||||
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) }
|
||||
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
|
||||
|
||||
let!(:hit_post) do
|
||||
Post.create!(uploaded_user: user, title: 'hello spec world',
|
||||
@@ -168,15 +147,12 @@ RSpec.describe 'Posts API', type: :request 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)
|
||||
deprecated_tag = create(:tag, deprecated_at: Time.current)
|
||||
create(:post_tag, post: hit_post, tag: deprecated_tag)
|
||||
|
||||
get '/posts'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
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' }
|
||||
|
||||
expect(tag_json.fetch('children')).to eq([])
|
||||
@@ -185,26 +161,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
])
|
||||
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
|
||||
it 'filters posts by q (hit case)' do
|
||||
get '/posts', params: { tags: 'spec_tag' }
|
||||
@@ -253,110 +209,18 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with legacy external tag name searches' do
|
||||
let!(:external_tag) { create(:external_tag, id: tag.id, name: 'search_external') }
|
||||
let!(:both_post) do
|
||||
create(:post).tap do |post|
|
||||
PostTag.create!(post:, tag:)
|
||||
PostExternalTag.create!(post:, external_tag:)
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
PostExternalTag.create!(post: miss_post, external_tag:)
|
||||
end
|
||||
|
||||
it 'keeps internal name searches independent of colliding external ids' do
|
||||
get '/posts', params: { tags: primary_tag_name_for(tag, 'ja').name }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(3)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(post_record.id, hit_post.id, both_post.id)
|
||||
end
|
||||
|
||||
it 'finds posts through PostExternalTag by the qualified legacy name' do
|
||||
get '/posts', params: { tags: 'nico:search_external' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(miss_post.id, both_post.id)
|
||||
end
|
||||
|
||||
[nil, 'all'].each do |match|
|
||||
it "intersects internal and external matches with match=#{ match || 'omitted' }" do
|
||||
params = { tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external" }
|
||||
params[:match] = match if match
|
||||
|
||||
get '/posts', params: params
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(1)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') }).to eq([both_post.id])
|
||||
end
|
||||
end
|
||||
|
||||
it 'unions internal alias and external matches without duplicate posts' do
|
||||
get '/posts', params: { tags: 'manko nico:search_external', match: 'any' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(4)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(post_record.id, hit_post.id, miss_post.id, both_post.id)
|
||||
end
|
||||
|
||||
it 'excludes external matches from an internal tag search' do
|
||||
get '/posts', params: {
|
||||
tags: "#{ primary_tag_name_for(tag, 'ja').name } not:nico:search_external" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(post_record.id, hit_post.id)
|
||||
end
|
||||
|
||||
it 'unions an external match with a negated internal match' do
|
||||
get '/posts', params: {
|
||||
tags: "nico:search_external not:#{ primary_tag_name_for(tag, 'ja').name }",
|
||||
match: 'any' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(miss_post.id, both_post.id)
|
||||
end
|
||||
|
||||
it 'keeps a missing qualified external name empty' do
|
||||
get '/posts', params: { tags: 'nico:missing_search_external' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(0)
|
||||
expect(json.fetch('posts')).to be_empty
|
||||
end
|
||||
|
||||
it 'applies the same mixed name search to the existing random endpoint' do
|
||||
get '/posts/random', params: {
|
||||
tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external",
|
||||
match: 'all' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('id')).to eq(both_post.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tags contain not:' do
|
||||
let!(:foo_tag_name) { create(:tag_name, name: 'not_spec_foo') }
|
||||
let!(:foo_tag) { create(:tag, primary_tag_name: foo_tag_name, category: :general) }
|
||||
let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') }
|
||||
let!(:foo_tag) { Tag.create!(tag_name: foo_tag_name, category: :general) }
|
||||
|
||||
let!(:bar_tag_name) { create(:tag_name, name: 'not_spec_bar') }
|
||||
let!(:bar_tag) { create(:tag, primary_tag_name: bar_tag_name, category: :general) }
|
||||
let!(:bar_tag_name) { TagName.create!(name: 'not_spec_bar') }
|
||||
let!(:bar_tag) { Tag.create!(tag_name: bar_tag_name, category: :general) }
|
||||
|
||||
let!(:baz_tag_name) { create(:tag_name, name: 'not_spec_baz') }
|
||||
let!(:baz_tag) { create(:tag, primary_tag_name: baz_tag_name, category: :general) }
|
||||
let!(:baz_tag_name) { TagName.create!(name: 'not_spec_baz') }
|
||||
let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) }
|
||||
|
||||
let!(:foo_alias_tag_name) do
|
||||
create(:tag_name, :alias, name: 'not_spec_foo_alias', tag: foo_tag)
|
||||
TagName.create!(name: 'not_spec_foo_alias', canonical: foo_tag_name)
|
||||
end
|
||||
|
||||
let!(:foo_only_post) do
|
||||
@@ -595,71 +459,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
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
|
||||
# 注意: controller の現状ロジックに合わせてる
|
||||
# original_created_from は `original_created_before > ?`
|
||||
@@ -724,32 +523,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns internal and external tags with colliding ids in the legacy tags array' do
|
||||
external_tag = create(:external_tag, id: tag.id, name: 'post_index_external')
|
||||
PostExternalTag.create!(post: hit_post, external_tag:)
|
||||
|
||||
get '/posts'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
post_json =
|
||||
json
|
||||
.fetch('posts')
|
||||
.find { _1.fetch('id') == hit_post.id }
|
||||
|
||||
external_json =
|
||||
post_json
|
||||
.fetch('tags')
|
||||
.find { _1['name'] == 'nico:post_index_external' }
|
||||
|
||||
expect(post_json.fetch('tags')).to include(
|
||||
a_hash_including('id' => tag.id,
|
||||
'name' => primary_tag_name_for(tag, 'ja').name,
|
||||
'category' => 'general'))
|
||||
expect(external_json).not_to be_nil
|
||||
expect_external_tag_json(external_json, external_tag)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /posts/:id' do
|
||||
@@ -779,10 +552,11 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
|
||||
it 'omits deprecated tags' do
|
||||
deprecated_tag = create(:tag,
|
||||
primary_name: 'deprecated_post_tag',
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'deprecated_post_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current)
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
PostTag.create!(post: post_record, tag: deprecated_tag)
|
||||
|
||||
request
|
||||
@@ -859,9 +633,9 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
tags =
|
||||
15.times.map do |i|
|
||||
tag_name = create(:tag_name, name: "show_query_tag_#{ i }")
|
||||
tag = create(:tag, primary_tag_name: tag_name, category: :general)
|
||||
create(:tag_name, :alias, name: "show_query_alias_#{ i }", tag:)
|
||||
tag_name = TagName.create!(name: "show_query_tag_#{ i }")
|
||||
tag = Tag.create!(tag_name:, category: :general)
|
||||
TagName.create!(name: "show_query_alias_#{ i }", canonical: tag_name)
|
||||
PostTag.create!(post: post_record, tag:)
|
||||
tag
|
||||
end
|
||||
@@ -906,27 +680,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(query_count).to be <= 45
|
||||
end
|
||||
|
||||
it 'returns external tags as root nodes in the legacy tag tree' do
|
||||
external_tag = create(:external_tag, id: tag.id, name: 'post_detail_external')
|
||||
PostExternalTag.create!(post: post_record, external_tag:)
|
||||
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(json.fetch('tags')).to include(
|
||||
a_hash_including('id' => tag.id,
|
||||
'name' => primary_tag_name_for(tag, 'ja').name,
|
||||
'category' => 'general'))
|
||||
external_json =
|
||||
json
|
||||
.fetch('tags')
|
||||
.find { _1['name'] == 'nico:post_detail_external' }
|
||||
|
||||
expect(external_json).not_to be_nil
|
||||
expect_external_tag_json(external_json, external_tag)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when post does not exist' do
|
||||
@@ -1008,7 +761,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
describe 'POST /posts' do
|
||||
let(:member) { create(:user, :member) }
|
||||
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
|
||||
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
@@ -1165,10 +918,11 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
|
||||
it 'rejects a deprecated tag specified directly' do
|
||||
create(:tag,
|
||||
primary_name: 'deprecated_direct_tag',
|
||||
Tag.create!(
|
||||
name: 'deprecated_direct_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current)
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
sign_in_as(member)
|
||||
|
||||
post '/posts', params: post_write_params(
|
||||
@@ -1185,16 +939,18 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
|
||||
it 'expands through multiple deprecated parent tags and saves active ancestors' do
|
||||
child = create(:tag, primary_name: 'active_child', category: :general)
|
||||
deprecated_parent = create(:tag,
|
||||
primary_name: 'deprecated_parent',
|
||||
child = Tag.create!(name: 'active_child', category: :general)
|
||||
deprecated_parent = Tag.create!(
|
||||
name: 'deprecated_parent',
|
||||
category: :general,
|
||||
deprecated_at: Time.current)
|
||||
deprecated_grandparent = create(:tag,
|
||||
primary_name: 'deprecated_grandparent',
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
deprecated_grandparent = Tag.create!(
|
||||
name: 'deprecated_grandparent',
|
||||
category: :general,
|
||||
deprecated_at: Time.current)
|
||||
active_grandparent = create(:tag, primary_name: 'active_grandparent', category: :general)
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
active_grandparent = Tag.create!(name: 'active_grandparent', category: :general)
|
||||
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
|
||||
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
|
||||
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_grandparent)
|
||||
@@ -1208,8 +964,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
saved_names = TagName.where(tag_id: Post.find(json.fetch('id')).tags.select(:id),
|
||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
||||
saved_names = Post.find(json.fetch('id')).tags.map(&:name)
|
||||
expect(saved_names).to include('active_child', 'active_grandparent')
|
||||
expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent')
|
||||
end
|
||||
@@ -1312,8 +1067,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
created_post = Post.find(json.fetch('id'))
|
||||
tag = TagName.find_by!(name: '伊地知ニジカ',
|
||||
language_code: 'ja', primary_flg: true).tag
|
||||
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
|
||||
section = PostTagSection.find_by!(post: created_post, tag:)
|
||||
|
||||
expect(section.begin_ms).to eq(60_000)
|
||||
@@ -1333,8 +1087,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
created_post = Post.find(json.fetch('id'))
|
||||
tag = TagName.find_by!(name: '伊地知ニジカ',
|
||||
language_code: 'ja', primary_flg: true).tag
|
||||
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
|
||||
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
|
||||
end
|
||||
|
||||
@@ -1351,8 +1104,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
created_post = Post.find(json.fetch('id'))
|
||||
tag = TagName.find_by!(name: '伊地知ニジカ',
|
||||
language_code: 'ja', primary_flg: true).tag
|
||||
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
|
||||
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
|
||||
end
|
||||
|
||||
@@ -1409,9 +1161,11 @@ RSpec.describe 'Posts API', type: :request do
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the external nico tag already exists' do
|
||||
context 'when nico tag already exists in tags' do
|
||||
before do
|
||||
create(:external_tag, name: 'nico_tag')
|
||||
Tag.find_undiscard_or_create_by!(
|
||||
tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
|
||||
category: :nico)
|
||||
end
|
||||
|
||||
it 'returns 422 with tag field errors' do
|
||||
@@ -1631,7 +1385,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(arguments[:thumbnails].keys).to eq([0])
|
||||
expect(arguments[:host]).to eq('http://www.example.com')
|
||||
end
|
||||
expect(creator).to have_received(:run).with(Locale.nipponese)
|
||||
end
|
||||
|
||||
it 'rejects malformed manifests as a request-level error' do
|
||||
@@ -1665,11 +1418,9 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
it '200 and updates title + resync tags when member' do
|
||||
sign_in_as(member)
|
||||
create(:post_tag_section, post: post_record, tag:,
|
||||
begin_ms: 1000, end_ms: 2000)
|
||||
|
||||
tn2 = create(:tag_name, name: 'spec_tag_2')
|
||||
replacement_tag = create(:tag, primary_tag_name: tn2, category: :general)
|
||||
tn2 = TagName.create!(name: 'spec_tag_2')
|
||||
Tag.create!(tag_name: tn2, category: :general)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
@@ -1682,45 +1433,14 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
names = json['tags'].map { |n| n['name'] }
|
||||
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('tag_id' => tag.id,
|
||||
'sections' => [{ 'begin_ms' => 1000, 'end_ms' => 2000 }]))
|
||||
expect(versions.last.tags_json.map { |item| item.fetch('tag_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('tag_id') }
|
||||
end
|
||||
expect(snapshots.map { |ids| ids.include?(tag.id) }).to eq([true, false, true])
|
||||
end
|
||||
|
||||
it 'rejects a deprecated tag specified directly' do
|
||||
create(:tag,
|
||||
primary_name: 'deprecated_update_tag',
|
||||
Tag.create!(
|
||||
name: 'deprecated_update_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current)
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{ post_record.id }", params: post_update_params(
|
||||
@@ -1735,9 +1455,11 @@ RSpec.describe 'Posts API', type: :request do
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the external nico tag already exists' do
|
||||
context 'when nico tag already exists in tags' do
|
||||
before do
|
||||
create(:external_tag, name: 'nico_tag')
|
||||
Tag.find_undiscard_or_create_by!(
|
||||
tag_name: TagName.find_undiscard_or_create_by!(name: 'nico:nico_tag'),
|
||||
category: :nico)
|
||||
end
|
||||
|
||||
it 'returns 422 with tag field errors' do
|
||||
@@ -1975,7 +1697,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated by me',
|
||||
tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}")
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name}")
|
||||
|
||||
expect(response).to have_http_status(:conflict)
|
||||
|
||||
@@ -1995,8 +1717,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
current_tag = Tag.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'current_added_tag', category: :general)
|
||||
current_tag = Tag.find_or_create_by_tag_name!('current_added_tag', category: :general)
|
||||
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
||||
|
||||
PostVersionRecorder.record!(
|
||||
@@ -2007,8 +1728,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
|
||||
'incoming_added_tag'].join(' '))
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_added_tag")
|
||||
|
||||
expect(response).to have_http_status(:conflict)
|
||||
|
||||
@@ -2026,8 +1746,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
current_tag = Tag.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'current_merge_tag', category: :general)
|
||||
current_tag = Tag.find_or_create_by_tag_name!('current_merge_tag', category: :general)
|
||||
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
||||
|
||||
PostVersionRecorder.record!(
|
||||
@@ -2038,17 +1757,15 @@ RSpec.describe 'Posts API', type: :request do
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
|
||||
'incoming_merge_tag'].join(' '),
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_merge_tag",
|
||||
merge: '1')
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).to include('current_merge_tag')
|
||||
expect(names).to include('incoming_merge_tag')
|
||||
end
|
||||
@@ -2059,7 +1776,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
nico_tag = create_nico_tag!('nico:optimistic_lock_nico')
|
||||
PostExternalTag.create!(post: post_record, external_tag: nico_tag)
|
||||
PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
|
||||
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
@@ -2071,58 +1788,47 @@ RSpec.describe 'Posts API', type: :request do
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
|
||||
tags: "spec_tag #{ Tag.no_deerjikist.name }")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
|
||||
expect(post_record.external_tags).to contain_exactly(nico_tag)
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).to include(nico_tag.name)
|
||||
end
|
||||
|
||||
it 'keeps nico tags even when they are not included in PUT tags' do
|
||||
sign_in_as(member)
|
||||
|
||||
nico_tag = create_nico_tag!('nico:readonly_update_nico')
|
||||
PostExternalTag.create!(post: post_record, external_tag: nico_tag)
|
||||
PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated title',
|
||||
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
|
||||
tags: "spec_tag #{ Tag.no_deerjikist.name }")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
|
||||
expect(post_record.external_tags).to contain_exactly(nico_tag)
|
||||
|
||||
external_json =
|
||||
json
|
||||
.fetch('tags')
|
||||
.find { _1['name'] == "nico:#{ nico_tag.name }" }
|
||||
|
||||
expect(external_json).not_to be_nil
|
||||
expect_external_tag_json(external_json, nico_tag)
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).to include(nico_tag.name)
|
||||
end
|
||||
|
||||
it 'allows non-nico tags linked from nico tags to be removed by normal post update' do
|
||||
sign_in_as(member)
|
||||
|
||||
nico_tag = create_nico_tag!('nico:relation_source')
|
||||
linked_tag = Tag.find_or_create_by_tag_name!(
|
||||
Locale.nipponese, 'relation_linked_tag', category: :general)
|
||||
linked_tag = Tag.find_or_create_by_tag_name!('relation_linked_tag', category: :general)
|
||||
|
||||
NicoTagRelation.create!(nico_tag:, tag: linked_tag)
|
||||
PostExternalTag.create!(post: post_record, external_tag: nico_tag)
|
||||
PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
|
||||
PostTag.create!(post: post_record, tag: linked_tag, created_user: member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
@@ -2130,17 +1836,16 @@ RSpec.describe 'Posts API', type: :request do
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
|
||||
tags: "spec_tag #{ Tag.no_deerjikist.name }")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(post_record.external_tags).to contain_exactly(nico_tag)
|
||||
expect(names).to include(nico_tag.name)
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
|
||||
expect(names).not_to include(primary_tag_name_for(linked_tag, 'ja').name)
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).not_to include(linked_tag.name)
|
||||
end
|
||||
|
||||
it 'force-updates stale posts without base_version_no' do
|
||||
@@ -2156,7 +1861,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
title: 'forced title',
|
||||
tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}",
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name}",
|
||||
force: '1')
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
@@ -2173,29 +1878,123 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns viewed state and current tags with their sections' 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)
|
||||
|
||||
it '200 and returns viewed boolean' do
|
||||
get '/posts/random'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to have_key('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
|
||||
|
||||
describe 'GET /posts/changes' do
|
||||
it 'returns 404 for the retired history endpoint' do
|
||||
get '/posts/changes'
|
||||
let(:member) { create(:user, :member) }
|
||||
|
||||
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
|
||||
|
||||
@@ -2209,8 +2008,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
let(:oc_from) { Time.zone.local(2019, 12, 31, 0, 0, 0) }
|
||||
let(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) }
|
||||
|
||||
let!(:tag_name2) { create(:tag_name, name: 'spec_tag_2') }
|
||||
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :general) }
|
||||
let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') }
|
||||
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) }
|
||||
|
||||
def snapshot_tags(post)
|
||||
post.snapshot_tag_names.join(' ')
|
||||
@@ -2225,7 +2024,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
url: post.url,
|
||||
thumbnail_base: post.thumbnail_base,
|
||||
tags: snapshot_tags(post),
|
||||
tags_json: post.snapshot_tags_json,
|
||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||
original_created_from: post.original_created_from,
|
||||
original_created_before: post.original_created_before,
|
||||
@@ -2247,7 +2045,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
|
||||
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)
|
||||
post_record.update!(
|
||||
title: 'updated spec post',
|
||||
@@ -2356,88 +2154,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(first.fetch('created_at')).to eq(t_v1.iso8601)
|
||||
end
|
||||
|
||||
context 'with external tag history' do
|
||||
let(:external_id) { tag.id }
|
||||
let(:external) { create(:external_tag, id: external_id) }
|
||||
let(:external_post) { create(:post) }
|
||||
|
||||
before do
|
||||
PostExternalTag.create!(post: external_post, external_tag: external)
|
||||
PostVersionRecorder.record!(
|
||||
post: external_post, event_type: :create, created_by_user: member)
|
||||
|
||||
unrelated_post = create(:post)
|
||||
PostExternalTag.create!(post: unrelated_post, external_tag: create(:external_tag))
|
||||
PostVersionRecorder.record!(
|
||||
post: unrelated_post, event_type: :create, created_by_user: member)
|
||||
end
|
||||
|
||||
it 'prefers Tag over ExternalTag for the legacy tag parameter' do
|
||||
get '/posts/versions', params: { tag: tag.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(3)
|
||||
expect(json.fetch('versions').map { [_1.fetch('post_id'), _1.fetch('version_no')] })
|
||||
.to contain_exactly(
|
||||
[post_record.id, 1], [post_record.id, 2], [other_post_version.post_id, 1])
|
||||
end
|
||||
|
||||
it 'explicitly filters ExternalTag even when its id collides with Tag' do
|
||||
get '/posts/versions', params: { external_tag: external.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(1)
|
||||
expect(json.fetch('versions')).to contain_exactly(
|
||||
a_hash_including('post_id' => external_post.id, 'version_no' => 1))
|
||||
end
|
||||
|
||||
# Temporary compatibility shim until the frontend sends external_tag explicitly.
|
||||
context 'with legacy tag fallback and no internal Tag with the external id' do
|
||||
let(:external_id) { Tag.maximum(:id).to_i + 10_000 }
|
||||
|
||||
it 'falls back to ExternalTag for the legacy tag parameter' do
|
||||
expect(Tag.exists?(external.id)).to be(false)
|
||||
|
||||
get '/posts/versions', params: { tag: external.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(1)
|
||||
expect(json.fetch('versions')).to contain_exactly(
|
||||
a_hash_including('post_id' => external_post.id, 'version_no' => 1))
|
||||
end
|
||||
|
||||
[:tag, :external_tag].each do |parameter|
|
||||
it "includes external removal history through the legacy API's #{ parameter }" do
|
||||
external_post.post_external_tags.destroy_all
|
||||
PostVersionRecorder.record!(
|
||||
post: external_post.reload, event_type: :update, created_by_user: member)
|
||||
|
||||
get '/posts/versions', params: { parameter => external.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('versions')).to contain_exactly(
|
||||
a_hash_including('post_id' => external_post.id, 'version_no' => 1),
|
||||
a_hash_including('post_id' => external_post.id, 'version_no' => 2))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'can render history containing external identifiers' do
|
||||
PostExternalTag.create!(post: post_record, external_tag: create(:external_tag))
|
||||
post_record.update_columns(version_no: 2)
|
||||
PostVersionRecorder.record!(post: post_record,
|
||||
event_type: :update, created_by_user: member)
|
||||
|
||||
get '/posts/versions', params: { post: post_record.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(3)
|
||||
expect(json.fetch('versions').first.fetch('tags')).to include(
|
||||
'name' => primary_tag_name_for(tag2, 'ja').name, 'type' => 'context')
|
||||
end
|
||||
|
||||
it 'filters versions by tag when the current snapshot includes the tag' do
|
||||
get '/posts/versions', params: { post: post_record.id, tag: tag2.id }
|
||||
|
||||
@@ -2568,8 +2284,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
sign_in_as(member)
|
||||
base_version = create_post_version_for!(post_record)
|
||||
|
||||
tag_name2 = create(:tag_name, name: 'spec_tag_2')
|
||||
create(:tag, primary_tag_name: tag_name2, category: :general)
|
||||
tag_name2 = TagName.create!(name: 'spec_tag_2')
|
||||
Tag.create!(tag_name: tag_name2, category: :general)
|
||||
|
||||
expect do
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
@@ -2733,8 +2449,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
tag_name2 = create(:tag_name, name: 'spec_tag_2')
|
||||
tag2 = create(:tag, primary_tag_name: tag_name2, category: :general)
|
||||
tag_name2 = TagName.create!(name: 'spec_tag_2')
|
||||
tag2 = Tag.create!(tag_name: tag_name2, category: :general)
|
||||
|
||||
expect {
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
|
||||
@@ -80,6 +80,38 @@ RSpec.describe "TagChildren", type: :request do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when parent is nico' do
|
||||
before { stub_current_user(admin) }
|
||||
|
||||
let!(:parent) { create(:tag, :nico, name: 'nico:parent_ng') }
|
||||
let(:parent_id) { parent.id }
|
||||
let(:child_id) { child.id }
|
||||
|
||||
it 'returns 400 and does not create relation' do
|
||||
expect {
|
||||
do_request
|
||||
}.not_to change(TagImplication, :count)
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when child is nico' do
|
||||
before { stub_current_user(admin) }
|
||||
|
||||
let!(:child) { create(:tag, :nico, name: 'nico:child_ng') }
|
||||
let(:parent_id) { parent.id }
|
||||
let(:child_id) { child.id }
|
||||
|
||||
it 'returns 400 and does not create relation' do
|
||||
expect {
|
||||
do_request
|
||||
}.not_to change(TagImplication, :count)
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /tag_children" do
|
||||
@@ -154,5 +186,31 @@ RSpec.describe "TagChildren", type: :request do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when parent is nico' do
|
||||
before { stub_current_user(admin) }
|
||||
|
||||
let!(:parent) { create(:tag, :nico, name: 'nico:parent_ng_delete') }
|
||||
let(:parent_id) { parent.id }
|
||||
let(:child_id) { child.id }
|
||||
|
||||
it 'returns 400' do
|
||||
do_request
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when child is nico' do
|
||||
before { stub_current_user(admin) }
|
||||
|
||||
let!(:child) { create(:tag, :nico, name: 'nico:child_ng_delete') }
|
||||
let(:parent_id) { parent.id }
|
||||
let(:child_id) { child.id }
|
||||
|
||||
it 'returns 400' do
|
||||
do_request
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,13 +3,13 @@ require 'rails_helper'
|
||||
RSpec.describe 'TagVersions API', type: :request do
|
||||
let(:member) { create(:user, :member, name: 'version member') }
|
||||
|
||||
let!(:tag) { create(:tag, primary_name: 'tag_versions_target', category: :general) }
|
||||
let!(:other_tag) { create(:tag, primary_name: 'tag_versions_other', category: :general) }
|
||||
let!(:tag) { create(:tag, name: 'tag_versions_target', category: :general) }
|
||||
let!(:other_tag) { create(:tag, name: 'tag_versions_other', category: :general) }
|
||||
|
||||
let!(:parent_shared) { create(:tag, primary_name: 'parent_shared', category: :general) }
|
||||
let!(:parent_old) { create(:tag, primary_name: 'parent_old', category: :general) }
|
||||
let!(:parent_new) { create(:tag, primary_name: 'parent_new', category: :general) }
|
||||
let!(:other_parent) { create(:tag, primary_name: 'other_parent', category: :general) }
|
||||
let!(:parent_shared) { create(:tag, name: 'parent_shared', category: :general) }
|
||||
let!(:parent_old) { create(:tag, name: 'parent_old', category: :general) }
|
||||
let!(:parent_new) { create(:tag, name: 'parent_new', category: :general) }
|
||||
let!(:other_parent) { create(:tag, name: 'other_parent', category: :general) }
|
||||
|
||||
let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) }
|
||||
let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) }
|
||||
@@ -36,12 +36,6 @@ RSpec.describe 'TagVersions API', type: :request do
|
||||
category: category,
|
||||
deprecated_at: deprecated_at,
|
||||
aliases: Array(aliases).join(' '),
|
||||
names: [{ language_code: 'ja', name:, script_code: 'Jpan',
|
||||
primary_flg: true,
|
||||
auto_generated: false }] + Array(aliases).map { |alias_name|
|
||||
{ language_code: 'ja', name: alias_name, script_code: 'Jpan',
|
||||
primary_flg: false, auto_generated: false }
|
||||
},
|
||||
parent_tag_ids: Array(parent_tags).map(&:id).join(' '),
|
||||
created_by_user: created_by_user,
|
||||
created_at: created_at)
|
||||
@@ -217,7 +211,7 @@ RSpec.describe 'TagVersions API', type: :request do
|
||||
end
|
||||
|
||||
it 'returns empty when the specified tag has no versions' do
|
||||
fresh_tag = create(:tag, primary_name: 'no_versions_tag', category: :general)
|
||||
fresh_tag = create(:tag, name: 'no_versions_tag', category: :general)
|
||||
|
||||
get '/tags/versions', params: { id: fresh_tag.id }
|
||||
|
||||
@@ -238,8 +232,8 @@ RSpec.describe 'TagVersions API', type: :request do
|
||||
end
|
||||
|
||||
it 'does not create tag versions by wiki updates when tag has no versions yet' do
|
||||
wiki_tag_name = create(:tag_name, name: 'tag_versions_from_wiki')
|
||||
wiki_tag = create(:tag, primary_tag_name: wiki_tag_name, category: :general)
|
||||
wiki_tag_name = TagName.create!(name: 'tag_versions_from_wiki')
|
||||
wiki_tag = Tag.create!(tag_name: wiki_tag_name, category: :general)
|
||||
|
||||
wiki_page =
|
||||
Wiki::Commit.create_content!(
|
||||
|
||||
@@ -8,13 +8,13 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
end
|
||||
|
||||
def create_tag! name:, category: :general
|
||||
tag_name = create(:tag_name, name:)
|
||||
create(:tag, primary_tag_name: tag_name, category:)
|
||||
tag_name = TagName.create!(name:)
|
||||
Tag.create!(tag_name:, category:)
|
||||
end
|
||||
|
||||
def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: primary_tag_name_for(tag, 'ja'),
|
||||
tag_name: tag.tag_name,
|
||||
body:,
|
||||
created_by_user: user,
|
||||
message: 'init')
|
||||
@@ -43,7 +43,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
wiki_page.reload
|
||||
version = wiki_page.wiki_versions.order(:version_no).last
|
||||
|
||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after')
|
||||
expect(tag.name).to eq('patch_tag_wiki_after')
|
||||
expect(wiki_page.title).to eq('patch_tag_wiki_after')
|
||||
|
||||
expect(version).to have_attributes(
|
||||
@@ -72,7 +72,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
tag.reload
|
||||
wiki_page.reload
|
||||
|
||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_category_only')
|
||||
expect(tag.name).to eq('patch_tag_category_only')
|
||||
expect(tag.category).to eq('meme')
|
||||
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
|
||||
end
|
||||
@@ -101,7 +101,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
wiki_page.reload
|
||||
version = wiki_page.wiki_versions.order(:version_no).last
|
||||
|
||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after')
|
||||
expect(tag.name).to eq('put_tag_wiki_after')
|
||||
expect(wiki_page.title).to eq('put_tag_wiki_after')
|
||||
|
||||
expect(version).to have_attributes(
|
||||
@@ -134,7 +134,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
tag.reload
|
||||
wiki_page.reload
|
||||
|
||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_category_only')
|
||||
expect(tag.name).to eq('put_tag_category_only')
|
||||
expect(tag.category).to eq('meme')
|
||||
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
||||
let(:guest) { create(:user, role: :guest) }
|
||||
|
||||
before do
|
||||
primary_tag_name_for(tag, 'ja').update!(name: 'deerjika')
|
||||
tag.tag_name.update!(name: 'deerjika')
|
||||
end
|
||||
|
||||
describe 'GET /tags/:id/deerjikists' do
|
||||
@@ -226,14 +226,6 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
||||
[platform2, code2],
|
||||
)
|
||||
end
|
||||
|
||||
it 'locks the tag before replacing the complete list' do
|
||||
expect_any_instance_of(Tag).to receive(:lock!).and_call_original
|
||||
|
||||
do_request
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tag already has deerjikists' do
|
||||
@@ -307,81 +299,6 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when platform is outside the enum' do
|
||||
let(:payload) do
|
||||
[
|
||||
{ platform: 'invalid', code: code1 },
|
||||
]
|
||||
end
|
||||
|
||||
it 'returns 422 with an indexed platform error without changing the list' do
|
||||
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
|
||||
|
||||
expect {
|
||||
do_request
|
||||
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'deerjikists.0.platform' => [be_present],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a requested deerjikist belongs to another tag' do
|
||||
let!(:other_tag) { create(:tag, category: :deerjikist) }
|
||||
let!(:owned_deerjikist) do
|
||||
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
|
||||
end
|
||||
let!(:conflicting_deerjikist) do
|
||||
Deerjikist.create!(platform: platform2, code: code2, tag: other_tag)
|
||||
end
|
||||
let(:payload) do
|
||||
[
|
||||
{ platform: 'nico', code: 'new-code' },
|
||||
{ platform: platform2, code: code2 },
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
primary_tag_name_for(other_tag, 'ja').update!(name: 'existing-deerjikist')
|
||||
end
|
||||
|
||||
it 'returns an indexed 422 error and rolls back the complete replacement' do
|
||||
expect {
|
||||
do_request
|
||||
}.not_to change { Deerjikist.order(:platform, :code).pluck(:platform, :code, :tag_id) }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'deerjikists.1.code' => [include('existing-deerjikist')],
|
||||
)
|
||||
expect(owned_deerjikist.reload.tag_id).to eq(tag.id)
|
||||
expect(conflicting_deerjikist.reload.tag_id).to eq(other_tag.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a requested deerjikist already belongs to the same tag' do
|
||||
let!(:existing_deerjikist) do
|
||||
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
|
||||
end
|
||||
let(:payload) do
|
||||
[
|
||||
{ platform: platform1, code: code1 },
|
||||
]
|
||||
end
|
||||
|
||||
it 'keeps the existing row' do
|
||||
expect {
|
||||
do_request
|
||||
}.not_to change { existing_deerjikist.reload.created_at }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(Deerjikist.where(tag: tag).pluck(:platform, :code))
|
||||
.to eq([[platform1, code1]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when youtube code is handle' do
|
||||
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
|
||||
let(:payload) do
|
||||
|
||||
+268
-448
ファイル差分が大きすぎるため省略します
差分を読み込み
@@ -396,7 +396,7 @@ RSpec.describe 'Theatres API', type: :request do
|
||||
end
|
||||
|
||||
it 'finalizes skip when votes reach majority and stores voters and tag snapshots' do
|
||||
tag = create(:tag, primary_name: 'skip-target')
|
||||
tag = create(:tag, name: 'skip-target')
|
||||
PostTag.create!(post: niconico_post, tag:)
|
||||
|
||||
TheatreSkipVote.create!(theatre:, post: niconico_post, user: member)
|
||||
@@ -503,7 +503,7 @@ RSpec.describe 'Theatres API', type: :request do
|
||||
end
|
||||
|
||||
it 'returns tag penalties and candidate weights for the current watchers' do
|
||||
tag = create(:tag, primary_name: 'heavy-tag')
|
||||
tag = create(:tag, name: 'heavy-tag')
|
||||
PostTag.create!(post: second_niconico_post, tag:)
|
||||
event = TheatreSkipEvent.create!(
|
||||
theatre:,
|
||||
|
||||
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki conflict handling', type: :request do
|
||||
it 'returns 409 when base_revision_id is stale' do
|
||||
page =
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: create(:tag_name, name: 'wiki_conflict_request'),
|
||||
tag_name: TagName.create!(name: 'wiki_conflict_request'),
|
||||
body: 'first',
|
||||
created_by_user: user,
|
||||
message: 'init')
|
||||
|
||||
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki history integrity', type: :request do
|
||||
|
||||
def create_wiki_page title:, body: 'body', message: 'init', user: self.user
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: create(:tag_name, name: title),
|
||||
tag_name: TagName.create!(name: title),
|
||||
body:,
|
||||
created_by_user: user,
|
||||
message:)
|
||||
|
||||
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
|
||||
let!(:user) { create_member_user! }
|
||||
|
||||
let!(:tn) { create(:tag_name, name: 'spec_wiki_title') }
|
||||
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
|
||||
let!(:page) do
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: tn,
|
||||
@@ -19,10 +19,11 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
message: 'init')
|
||||
end
|
||||
let!(:tag) do
|
||||
create(:tag,
|
||||
primary_tag_name: tn,
|
||||
Tag.create!(
|
||||
tag_name: tn,
|
||||
category: :general,
|
||||
deprecated_at: Time.zone.local(2026, 6, 1))
|
||||
deprecated_at: Time.zone.local(2026, 6, 1)
|
||||
)
|
||||
end
|
||||
|
||||
describe 'GET /wiki' do
|
||||
@@ -207,7 +208,7 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
{ 'X-Transfer-Code' => user.inheritance_code }
|
||||
end
|
||||
|
||||
let!(:test_tag_name) { create(:tag_name, name: 'TestPage') }
|
||||
let!(:test_tag_name) { TagName.create!(name: 'TestPage') }
|
||||
|
||||
let!(:page) do
|
||||
Wiki::Commit.create_content!(
|
||||
@@ -274,8 +275,8 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
end
|
||||
|
||||
it 'wiki body だけを変更しても tag version は作成しない' do
|
||||
linked_tag_name = create(:tag_name, name: 'wiki_body_only_tag')
|
||||
linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general)
|
||||
linked_tag_name = TagName.create!(name: 'wiki_body_only_tag')
|
||||
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
|
||||
|
||||
TagVersionRecorder.record!(
|
||||
tag: linked_tag,
|
||||
@@ -358,13 +359,13 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
describe 'GET /wiki/search' do
|
||||
before do
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: create(:tag_name, name: 'spec_wiki_title_2'),
|
||||
tag_name: TagName.create!(name: 'spec_wiki_title_2'),
|
||||
body: 'search body 2',
|
||||
created_by_user: user,
|
||||
message: 'init')
|
||||
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: create(:tag_name, name: 'unrelated_title'),
|
||||
tag_name: TagName.create!(name: 'unrelated_title'),
|
||||
body: 'unrelated body',
|
||||
created_by_user: user,
|
||||
message: 'init')
|
||||
@@ -433,7 +434,7 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
|
||||
it 'returns empty array when page has no revisions and filtered by id' do
|
||||
# 別ページを作って revision 無し
|
||||
tn2 = create(:tag_name, name: 'spec_no_rev')
|
||||
tn2 = TagName.create!(name: 'spec_no_rev')
|
||||
# 異常データ: revision 無し WikiPage を直接作る
|
||||
p2 = WikiPage.create!(
|
||||
tag_name: tn2,
|
||||
@@ -513,7 +514,7 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
|
||||
describe 'Wiki::Commit.redirect!' do
|
||||
it 'raises because redirect revisions are deprecated' do
|
||||
target_tag_name = create(:tag_name, name: 'redirect_deprecated_target')
|
||||
target_tag_name = TagName.create!(name: 'redirect_deprecated_target')
|
||||
target =
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: target_tag_name,
|
||||
@@ -534,8 +535,8 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
end
|
||||
|
||||
it 'wiki title を変更すると対応する tag の version を作成する' do
|
||||
linked_tag_name = create(:tag_name, name: 'wiki_linked_tag_for_version')
|
||||
linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general)
|
||||
linked_tag_name = TagName.create!(name: 'wiki_linked_tag_for_version')
|
||||
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
|
||||
|
||||
linked_page =
|
||||
Wiki::Commit.create_content!(
|
||||
@@ -563,8 +564,7 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
linked_tag.reload
|
||||
expect(primary_tag_name_for(linked_tag, 'ja').name)
|
||||
.to eq('wiki_linked_tag_for_version_renamed')
|
||||
expect(linked_tag.name).to eq('wiki_linked_tag_for_version_renamed')
|
||||
|
||||
versions = linked_tag.tag_versions.order(:version_no)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ RSpec.describe 'Wiki title collision', type: :request do
|
||||
|
||||
def create_wiki_page title:, body:
|
||||
Wiki::Commit.create_content!(
|
||||
tag_name: create(:tag_name, name: title),
|
||||
tag_name: TagName.create!(name: title),
|
||||
body:,
|
||||
created_by_user: user,
|
||||
message: 'init')
|
||||
|
||||
変更されたファイルが多すぎるため、一部のファイルは表示されません さらに表示
新しいイシューから参照
ユーザーをブロックする