コミットを比較
14
コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
a0f1d0c4c3 | ||
|
|
b04f3e698d | ||
|
|
5d2e85a690 | ||
|
|
3b2acb95be | ||
|
|
1de00191ea | ||
|
|
d1bab299ab | ||
|
|
089727c153 | ||
|
|
894f9f9571 | ||
|
|
2490ed91c4 | ||
|
|
2dbd5260ea | ||
|
|
2e25c6e0ba | ||
|
|
a0c2788e00 | ||
|
|
31fc32b377 | ||
|
|
068355720d |
@@ -92,6 +92,4 @@ class ApplicationController < ActionController::API
|
|||||||
|
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
class DeerjikistsController < ApplicationController
|
class DeerjikistsController < ApplicationController
|
||||||
def show
|
def show
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
platform = params[:platform].to_s.strip
|
platform = params[:platform].to_s.strip
|
||||||
code = params[:code].to_s.strip
|
code = params[:code].to_s.strip
|
||||||
return render_bad_request('platform は必須です.') if platform.blank?
|
return render_bad_request('platform は必須です.') if platform.blank?
|
||||||
@@ -9,10 +7,10 @@ class DeerjikistsController < ApplicationController
|
|||||||
|
|
||||||
deerjikist = Deerjikist
|
deerjikist = Deerjikist
|
||||||
.joins(:tag)
|
.joins(:tag)
|
||||||
.includes(tag: :tag_names)
|
.includes(tag: :tag_name)
|
||||||
.find_by(platform:, code:)
|
.find_by(platform:, code:)
|
||||||
if deerjikist
|
if deerjikist
|
||||||
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
|
render json: DeerjikistRepr.base(deerjikist)
|
||||||
else
|
else
|
||||||
head :not_found
|
head :not_found
|
||||||
end
|
end
|
||||||
@@ -22,7 +20,6 @@ class DeerjikistsController < ApplicationController
|
|||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
platform = params[:platform].to_s.strip
|
platform = params[:platform].to_s.strip
|
||||||
code = params[:code].to_s.strip
|
code = params[:code].to_s.strip
|
||||||
tag_id = params[:tag_id].to_i
|
tag_id = params[:tag_id].to_i
|
||||||
@@ -35,7 +32,7 @@ class DeerjikistsController < ApplicationController
|
|||||||
d.save!
|
d.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
|
render json: DeerjikistRepr.base(deerjikist)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class GekanatorPostsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
posts =
|
posts =
|
||||||
Post
|
Post
|
||||||
.preload(:post_similarities, tags: :tag_names)
|
.preload(:post_similarities, tags: :tag_name)
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
.order(Arel.sql(
|
.order(Arel.sql(
|
||||||
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
|
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
|
||||||
@@ -56,7 +56,7 @@ class GekanatorPostsController < ApplicationController
|
|||||||
def tag_json tag
|
def tag_json tag
|
||||||
{
|
{
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
name: tag.name('ja'),
|
name: tag.name,
|
||||||
category: tag.category
|
category: tag.category
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ class GekanatorQuestionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.where(category: categories.uniq)
|
.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)
|
.where.not(deprecated_at: nil)
|
||||||
.pluck('tags.category', 'tag_names.name')
|
.pluck('tags.category', 'tag_names.name')
|
||||||
.each_with_object({ }) do |(category, name), h|
|
.each_with_object({ }) do |(category, name), h|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ class MaterialsController < ApplicationController
|
|||||||
rescue_from MaterialZipExporter::MissingFileError, with: :render_zip_missing_file
|
rescue_from MaterialZipExporter::MissingFileError, with: :render_zip_missing_file
|
||||||
|
|
||||||
def index
|
def index
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
page = (params[:page].presence || 1).to_i
|
page = (params[:page].presence || 1).to_i
|
||||||
limit = (params[:limit].presence || 20).to_i
|
limit = (params[:limit].presence || 20).to_i
|
||||||
|
|
||||||
@@ -22,7 +20,7 @@ class MaterialsController < ApplicationController
|
|||||||
q = Material.includes(:material_export_items,
|
q = Material.includes(:material_export_items,
|
||||||
thumbnail_attachment: :blob,
|
thumbnail_attachment: :blob,
|
||||||
file_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(tag_id: nil) if filters[:tag_state] == 'untagged'
|
||||||
q = q.where.not(tag_id: nil) if filters[:tag_state] == 'tagged'
|
q = q.where.not(tag_id: nil) if filters[:tag_state] == 'tagged'
|
||||||
q = apply_material_tag_filter(q, filters, tag_graph)
|
q = apply_material_tag_filter(q, filters, tag_graph)
|
||||||
@@ -44,10 +42,7 @@ class MaterialsController < ApplicationController
|
|||||||
.offset(offset)
|
.offset(offset)
|
||||||
.to_a
|
.to_a
|
||||||
|
|
||||||
response = { materials: MaterialRepr.list_many(
|
response = { materials: MaterialRepr.list_many(materials, host: request.base_url),
|
||||||
materials,
|
|
||||||
host: request.base_url,
|
|
||||||
language_code: locale.language_code),
|
|
||||||
count: }
|
count: }
|
||||||
if filters[:tag_id]
|
if filters[:tag_id]
|
||||||
response[:tag_scope] = material_index_tag_scope(filters, tag_graph)
|
response[:tag_scope] = material_index_tag_scope(filters, tag_graph)
|
||||||
@@ -60,8 +55,6 @@ class MaterialsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
material =
|
material =
|
||||||
Material
|
Material
|
||||||
.includes(:tag, :material_export_items)
|
.includes(:tag, :material_export_items)
|
||||||
@@ -70,17 +63,15 @@ class MaterialsController < ApplicationController
|
|||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
return head :not_found unless material
|
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,
|
render json: MaterialRepr.base(material, host: request.base_url).merge(wiki_page_body:)
|
||||||
language_code: locale.language_code).merge(wiki_page_body:)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
tag_name_raw = params[:tag].to_s.strip
|
tag_name_raw = params[:tag].to_s.strip
|
||||||
file = params[:file]
|
file = params[:file]
|
||||||
file_sha256 = MaterialFileSha256.from_upload(file)
|
file_sha256 = MaterialFileSha256.from_upload(file)
|
||||||
@@ -98,14 +89,13 @@ class MaterialsController < ApplicationController
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
Material.transaction do
|
Material.transaction do
|
||||||
tag = resolve_material_tag!(locale, tag_name_raw)
|
tag = resolve_material_tag!(tag_name_raw)
|
||||||
material = Material.new(tag:, url:,
|
material = Material.new(tag:, url:,
|
||||||
created_by_user: current_user,
|
created_by_user: current_user,
|
||||||
updated_by_user: current_user)
|
updated_by_user: current_user)
|
||||||
material.file.attach(uploaded_blob) if uploaded_blob
|
material.file.attach(uploaded_blob) if uploaded_blob
|
||||||
material.save!
|
material.save!
|
||||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
|
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
|
||||||
language_code: locale.language_code)
|
|
||||||
upsert_export_paths!(material)
|
upsert_export_paths!(material)
|
||||||
MaterialVersionRecorder.record!(material:, event_type: :create,
|
MaterialVersionRecorder.record!(material:, event_type: :create,
|
||||||
created_by_user: current_user)
|
created_by_user: current_user)
|
||||||
@@ -117,8 +107,7 @@ class MaterialsController < ApplicationController
|
|||||||
|
|
||||||
if material
|
if material
|
||||||
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
||||||
render json: MaterialRepr.base(material, host: request.base_url,
|
render json: MaterialRepr.base(material, host: request.base_url), status: :created
|
||||||
language_code: locale.language_code), status: :created
|
|
||||||
else
|
else
|
||||||
render_validation_error material
|
render_validation_error material
|
||||||
end
|
end
|
||||||
@@ -143,14 +132,12 @@ class MaterialsController < ApplicationController
|
|||||||
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
|
||||||
return render_material_import_block(block) if block
|
return render_material_import_block(block) if block
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
uploaded_blob = build_uploaded_material_blob!(file, file_sha256)
|
uploaded_blob = build_uploaded_material_blob!(file, file_sha256)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Material.transaction do
|
Material.transaction do
|
||||||
MaterialVersionRecorder.ensure_snapshot!(material, created_by_user: current_user)
|
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)
|
material.assign_attributes(tag:, url:, updated_by_user: current_user)
|
||||||
if uploaded_blob
|
if uploaded_blob
|
||||||
material.file.attach(uploaded_blob)
|
material.file.attach(uploaded_blob)
|
||||||
@@ -158,8 +145,7 @@ class MaterialsController < ApplicationController
|
|||||||
material.file.detach
|
material.file.detach
|
||||||
end
|
end
|
||||||
material.save!
|
material.save!
|
||||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
|
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
|
||||||
language_code: locale.language_code)
|
|
||||||
upsert_export_paths!(material)
|
upsert_export_paths!(material)
|
||||||
MaterialVersionRecorder.record!(material:, event_type: :update,
|
MaterialVersionRecorder.record!(material:, event_type: :update,
|
||||||
created_by_user: current_user)
|
created_by_user: current_user)
|
||||||
@@ -171,8 +157,7 @@ class MaterialsController < ApplicationController
|
|||||||
|
|
||||||
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
||||||
|
|
||||||
render json: MaterialRepr.base(material, host: request.base_url,
|
render json: MaterialRepr.base(material, host: request.base_url)
|
||||||
language_code: locale.language_code)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -251,8 +236,10 @@ class MaterialsController < ApplicationController
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_material_tag! locale, tag_name_raw
|
def resolve_material_tag! tag_name_raw
|
||||||
Tag.find_or_create_by_tag_name!(locale, tag_name_raw, category: :material)
|
tag_name = TagName.find_or_create_by!(name: tag_name_raw)
|
||||||
|
tag = tag_name.tag
|
||||||
|
tag || Tag.create!(tag_name:, category: :material)
|
||||||
end
|
end
|
||||||
|
|
||||||
def material_index_needs_tag_name? filters
|
def material_index_needs_tag_name? filters
|
||||||
@@ -266,12 +253,7 @@ class MaterialsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def material_index_join_tag_name q
|
def material_index_join_tag_name q
|
||||||
q.left_joins(:tag).joins(<<~SQL.squish)
|
q.left_joins(tag: :tag_name)
|
||||||
LEFT JOIN tag_names
|
|
||||||
ON tag_names.tag_id = tags.id
|
|
||||||
AND tag_names.language_code = 'ja'
|
|
||||||
AND tag_names.primary_flg = TRUE
|
|
||||||
SQL
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def material_index_join_file_blob q
|
def material_index_join_file_blob q
|
||||||
@@ -287,11 +269,9 @@ class MaterialsController < ApplicationController
|
|||||||
|
|
||||||
def apply_material_query q, term
|
def apply_material_query q, term
|
||||||
like = "%#{ ActiveRecord::Base.sanitize_sql_like(term) }%"
|
like = "%#{ ActiveRecord::Base.sanitize_sql_like(term) }%"
|
||||||
q.where(<<~SQL.squish, q: like)
|
q.where('tag_names.name LIKE :q OR materials.url LIKE :q OR ' \
|
||||||
tag_names.name LIKE :q
|
'material_file_blobs.filename LIKE :q',
|
||||||
OR materials.url LIKE :q
|
q: like)
|
||||||
OR material_file_blobs.filename LIKE :q
|
|
||||||
SQL
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_material_media_kind q, media_kind
|
def apply_material_media_kind q, media_kind
|
||||||
@@ -343,9 +323,8 @@ class MaterialsController < ApplicationController
|
|||||||
end
|
end
|
||||||
tags_by_id =
|
tags_by_id =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.where(id: scope_tag_ids,
|
.where(id: scope_tag_ids)
|
||||||
tag_names: { language_code: 'ja', primary_flg: true })
|
|
||||||
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
||||||
.each_with_object({ }) do |(id, name, category, deprecated_at), hash|
|
.each_with_object({ }) do |(id, name, category, deprecated_at), hash|
|
||||||
hash[id] = { id:, name:, category:, deprecated: deprecated_at.present? }
|
hash[id] = { id:, name:, category:, deprecated: deprecated_at.present? }
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
class NicoTagsController < ApplicationController
|
class NicoTagsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
name = params[:name].presence
|
name = params[:name].presence
|
||||||
linked_tag = params[:linked_tag].presence
|
linked_tag = params[:linked_tag].presence
|
||||||
link_status = params[:link_status].presence
|
link_status = params[:link_status].presence
|
||||||
@@ -26,7 +24,7 @@ class NicoTagsController < ApplicationController
|
|||||||
ExternalTag
|
ExternalTag
|
||||||
.joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \
|
.joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \
|
||||||
'ON post_tag_max.external_tag_id = external_tags.id')
|
'ON post_tag_max.external_tag_id = external_tags.id')
|
||||||
.includes(linked_tags: { tag_names: :wiki_page })
|
.includes(linked_tags: { tag_name: :wiki_page })
|
||||||
if name
|
if name
|
||||||
q = q.where(('external_tags.name LIKE ? ' +
|
q = q.where(('external_tags.name LIKE ? ' +
|
||||||
"OR CONCAT(external_tags.platform, ':', external_tags.name) LIKE ?"),
|
"OR CONCAT(external_tags.platform, ':', external_tags.name) LIKE ?"),
|
||||||
@@ -36,8 +34,7 @@ class NicoTagsController < ApplicationController
|
|||||||
if linked_tag
|
if linked_tag
|
||||||
linked_tag_ids =
|
linked_tag_ids =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.where(tag_names: { language_code: 'ja', primary_flg: true })
|
|
||||||
.where('tag_names.name LIKE ?', "%#{ linked_tag }%")
|
.where('tag_names.name LIKE ?', "%#{ linked_tag }%")
|
||||||
.pluck(:id)
|
.pluck(:id)
|
||||||
linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id)
|
linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id)
|
||||||
@@ -70,9 +67,7 @@ class NicoTagsController < ApplicationController
|
|||||||
render json: { tags: tags.map { |tag|
|
render json: { tags: tags.map { |tag|
|
||||||
external_tag_json(tag).merge(
|
external_tag_json(tag).merge(
|
||||||
recent_post_tag_created_at: tag.recent_post_tag_created_at,
|
recent_post_tag_created_at: tag.recent_post_tag_created_at,
|
||||||
linked_tags: tag.linked_tags.map { |lt|
|
linked_tags: tag.linked_tags.map { |lt| TagRepr.base(lt) })
|
||||||
TagRepr.base(lt, language_code: locale.language_code)
|
|
||||||
})
|
|
||||||
}, count: }
|
}, count: }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -86,8 +81,6 @@ class NicoTagsController < ApplicationController
|
|||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
id = params[:id].to_i
|
id = params[:id].to_i
|
||||||
|
|
||||||
tag = ExternalTag.find(id)
|
tag = ExternalTag.find(id)
|
||||||
@@ -96,14 +89,10 @@ class NicoTagsController < ApplicationController
|
|||||||
linked_tags = nil
|
linked_tags = nil
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
linked_tags = Tag.normalise_tags!(locale, linked_tag_names,
|
linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false,
|
||||||
with_tagme: false,
|
|
||||||
with_no_deerjikist: false)
|
with_no_deerjikist: false)
|
||||||
|
|
||||||
TagVersioning.record_tag_snapshots!(
|
TagVersioning.record_tag_snapshots!(linked_tags, created_by_user: current_user)
|
||||||
linked_tags,
|
|
||||||
created_by_user: current_user,
|
|
||||||
language_code: locale.language_code)
|
|
||||||
|
|
||||||
tag.linked_tags = linked_tags
|
tag.linked_tags = linked_tags
|
||||||
tag.save!
|
tag.save!
|
||||||
@@ -113,9 +102,7 @@ class NicoTagsController < ApplicationController
|
|||||||
created_by_user: current_user)
|
created_by_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: tag.linked_tags.map { |t|
|
render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok
|
||||||
TagRepr.base(t, language_code: locale.language_code)
|
|
||||||
}, status: :ok
|
|
||||||
rescue Tag::NicoTagNormalisationError
|
rescue Tag::NicoTagNormalisationError
|
||||||
render_validation_error fields: { tags: ['ニコニコ・タグ同士は連携できません.'] }
|
render_validation_error fields: { tags: ['ニコニコ・タグ同士は連携できません.'] }
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
url = params[:url].presence
|
url = params[:url].presence
|
||||||
title = params[:title].presence
|
title = params[:title].presence
|
||||||
original_created_from = params[:original_created_from].presence
|
original_created_from = params[:original_created_from].presence
|
||||||
@@ -48,12 +46,12 @@ class PostsController < ApplicationController
|
|||||||
'COALESCE(pt_max.max_updated_at, posts.updated_at))'
|
'COALESCE(pt_max.max_updated_at, posts.updated_at))'
|
||||||
|
|
||||||
q =
|
q =
|
||||||
filtered_posts(locale)
|
filtered_posts
|
||||||
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
|
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
|
||||||
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
|
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
|
||||||
.preload(:external_tags, :uploaded_user, :parents, :children,
|
.preload(:external_tags, :uploaded_user, :parents, :children,
|
||||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||||
{ tag_names: :wiki_page }] }])
|
{ tag_name: :wiki_page }] }])
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
|
|
||||||
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
||||||
@@ -92,8 +90,7 @@ class PostsController < ApplicationController
|
|||||||
q = q.except(:select, :order)
|
q = q.except(:select, :order)
|
||||||
|
|
||||||
render json: { posts: posts.map { |post|
|
render json: { posts: posts.map { |post|
|
||||||
PostRepr.base(post, language_code: locale.language_code)
|
PostRepr.base(post).merge(updated_at: post.updated_at_all).tap do |json|
|
||||||
.merge(updated_at: post.updated_at_all).tap do |json|
|
|
||||||
json['thumbnail'] =
|
json['thumbnail'] =
|
||||||
if post.thumbnail.attached?
|
if post.thumbnail.attached?
|
||||||
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
||||||
@@ -105,19 +102,17 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def random
|
def random
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
post =
|
post =
|
||||||
filtered_posts(locale)
|
filtered_posts
|
||||||
.preload(:uploaded_user, :parents, :children,
|
.preload(:uploaded_user, :parents, :children,
|
||||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||||
{ tag_names: :wiki_page }] }])
|
{ tag_name: :wiki_page }] }])
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
.order('RAND()')
|
.order('RAND()')
|
||||||
.first
|
.first
|
||||||
return head :not_found unless post
|
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
|
end
|
||||||
|
|
||||||
def metadata
|
def metadata
|
||||||
@@ -192,13 +187,11 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
post =
|
post =
|
||||||
Post
|
Post
|
||||||
.includes(:uploaded_user, :parents, :children,
|
.includes(:uploaded_user, :parents, :children,
|
||||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||||
{ tag_names: :wiki_page }] }])
|
{ tag_name: :wiki_page }] }])
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
return head :not_found unless post
|
return head :not_found unless post
|
||||||
@@ -212,21 +205,18 @@ class PostsController < ApplicationController
|
|||||||
parent_posts:,
|
parent_posts:,
|
||||||
child_posts:,
|
child_posts:,
|
||||||
sibling_posts:,
|
sibling_posts:,
|
||||||
related:,
|
related:)
|
||||||
language_code: locale.language_code)
|
.merge(tags: build_tag_tree_for(post))
|
||||||
.merge(tags: build_tag_tree_for(post, locale.language_code))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
preflight = PostCreatePreflight.new(
|
preflight = PostCreatePreflight.new(
|
||||||
attributes: post_create_attributes,
|
attributes: post_create_attributes,
|
||||||
thumbnail: params[:thumbnail],
|
thumbnail: params[:thumbnail],
|
||||||
host: request.base_url).run(locale)
|
host: request.base_url).run
|
||||||
return render json: dry_run_json(preflight) if bool?(:dry)
|
return render json: dry_run_json(preflight) if bool?(:dry)
|
||||||
if preflight[:existing_post_id].present?
|
if preflight[:existing_post_id].present?
|
||||||
post = Post.new(url: preflight[:url])
|
post = Post.new(url: preflight[:url])
|
||||||
@@ -252,10 +242,10 @@ class PostsController < ApplicationController
|
|||||||
:post_tag_specs,
|
:post_tag_specs,
|
||||||
:tag_sections,
|
:tag_sections,
|
||||||
:normalised_parent_post_ids).symbolize_keys).merge(
|
:normalised_parent_post_ids).symbolize_keys).merge(
|
||||||
thumbnail: params[:thumbnail])).create!(locale)
|
thumbnail: params[:thumbnail])).create!
|
||||||
|
|
||||||
post.reload
|
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
|
rescue PostCreatePreflight::ValidationFailed => e
|
||||||
render_validation_error fields: e.fields, base: e.base_errors
|
render_validation_error fields: e.fields, base: e.base_errors
|
||||||
rescue Tag::NicoTagNormalisationError
|
rescue Tag::NicoTagNormalisationError
|
||||||
@@ -281,18 +271,13 @@ class PostsController < ApplicationController
|
|||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
return head :unsupported_media_type unless request.content_mime_type == Mime[:multipart_form]
|
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
|
return head :payload_too_large if request.content_length.to_i > MAX_BULK_REQUEST_BYTES
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
posts = parse_bulk_posts_manifest
|
posts = parse_bulk_posts_manifest
|
||||||
thumbnails = parse_bulk_thumbnails(posts.length)
|
thumbnails = parse_bulk_thumbnails(posts.length)
|
||||||
|
|
||||||
result = PostBulkCreator.new(
|
result = PostBulkCreator.new(
|
||||||
actor: current_user,
|
actor: current_user,
|
||||||
posts:,
|
posts:,
|
||||||
thumbnails:,
|
thumbnails:,
|
||||||
host: request.base_url).run(locale)
|
host: request.base_url).run
|
||||||
|
|
||||||
render json: result
|
render json: result
|
||||||
rescue JSON::ParserError
|
rescue JSON::ParserError
|
||||||
render_bad_request 'posts manifest の JSON が不正です.'
|
render_bad_request 'posts manifest の JSON が不正です.'
|
||||||
@@ -325,7 +310,6 @@ class PostsController < ApplicationController
|
|||||||
base_version_no = parse_base_version_no
|
base_version_no = parse_base_version_no
|
||||||
return render_bad_request('base_version_no は必須です.') if !(force) && !(base_version_no)
|
return render_bad_request('base_version_no は必須です.') if !(force) && !(base_version_no)
|
||||||
|
|
||||||
locale = Locale.find_by(code: params[:locale].presence) || Locale.nipponese
|
|
||||||
title = params[:title].presence
|
title = params[:title].presence
|
||||||
tag_names = params[:tags].to_s.split
|
tag_names = params[:tags].to_s.split
|
||||||
original_created_from = params[:original_created_from]
|
original_created_from = params[:original_created_from]
|
||||||
@@ -345,10 +329,9 @@ class PostsController < ApplicationController
|
|||||||
base_version = post.post_versions.find_by!(version_no: base_version_no)
|
base_version = post.post_versions.find_by!(version_no: base_version_no)
|
||||||
|
|
||||||
base_snapshot = post_snapshot_from_version(base_version)
|
base_snapshot = post_snapshot_from_version(base_version)
|
||||||
current_snapshot = post_snapshot_from_record(post, locale)
|
current_snapshot = post_snapshot_from_record(post)
|
||||||
end
|
end
|
||||||
incoming_snapshot = post_incoming_snapshot(locale:,
|
incoming_snapshot = post_incoming_snapshot(title:,
|
||||||
title:,
|
|
||||||
original_created_from:,
|
original_created_from:,
|
||||||
original_created_before:,
|
original_created_before:,
|
||||||
tag_names:,
|
tag_names:,
|
||||||
@@ -377,14 +360,14 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
apply_post_snapshot!(locale, post, snapshot_to_apply)
|
apply_post_snapshot!(post, snapshot_to_apply)
|
||||||
end
|
end
|
||||||
|
|
||||||
return render json: conflict_json, status: :conflict if conflict_json
|
return render json: conflict_json, status: :conflict if conflict_json
|
||||||
|
|
||||||
post.reload
|
post.reload
|
||||||
json = PostRepr.base(post, current_user, language_code: locale.language_code)
|
json = PostRepr.base(post, current_user)
|
||||||
json['tags'] = build_tag_tree_for(post, locale.language_code)
|
json['tags'] = build_tag_tree_for(post)
|
||||||
render json:, status: :ok
|
render json:, status: :ok
|
||||||
rescue Tag::NicoTagNormalisationError
|
rescue Tag::NicoTagNormalisationError
|
||||||
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
|
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
|
||||||
@@ -402,19 +385,19 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filtered_posts locale
|
def filtered_posts
|
||||||
tag_names = params[:tags].to_s.split
|
tag_names = params[:tags].to_s.split
|
||||||
match_type = params[:match]
|
match_type = params[:match]
|
||||||
if tag_names.present?
|
if tag_names.present?
|
||||||
filter_posts_by_tags(locale, tag_names, match_type)
|
filter_posts_by_tags(tag_names, match_type)
|
||||||
else
|
else
|
||||||
Post.all
|
Post.all
|
||||||
end
|
end
|
||||||
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|
|
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:') }
|
negative: raw_name.downcase.start_with?('not:') }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -422,11 +405,11 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
if match_type == 'any'
|
if match_type == 'any'
|
||||||
literals.reduce(Post.none) do |posts, literal|
|
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
|
end
|
||||||
else
|
else
|
||||||
literals.reduce(Post.all) do |posts, literal|
|
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]
|
if literal[:negative]
|
||||||
posts.where.not(id: ids)
|
posts.where.not(id: ids)
|
||||||
else
|
else
|
||||||
@@ -436,8 +419,8 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_literal_relation locale, name, negative:
|
def tag_literal_relation name, negative:
|
||||||
ids = tagged_post_ids_for(locale, name)
|
ids = tagged_post_ids_for(name)
|
||||||
if negative
|
if negative
|
||||||
Post.where.not(id: ids)
|
Post.where.not(id: ids)
|
||||||
else
|
else
|
||||||
@@ -445,11 +428,11 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def tagged_post_ids_for locale, name
|
def tagged_post_ids_for(name)
|
||||||
posts_by_internal_tags =
|
posts_by_internal_tags =
|
||||||
Post
|
Post
|
||||||
.joins(tags: :tag_names)
|
.joins(tags: :tag_name)
|
||||||
.where(tag_names: { language_code: locale.language_code, name: })
|
.where(tag_names: { name: })
|
||||||
.select(:id)
|
.select(:id)
|
||||||
|
|
||||||
posts_by_external_tags =
|
posts_by_external_tags =
|
||||||
@@ -491,7 +474,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_tag_tree_for post, language_code = 'ja'
|
def build_tag_tree_for post
|
||||||
post_tags = post.post_tags.reject { |post_tag| post_tag.tag.deprecated? }
|
post_tags = post.post_tags.reject { |post_tag| post_tag.tag.deprecated? }
|
||||||
tags = post_tags.map(&:tag)
|
tags = post_tags.map(&:tag)
|
||||||
tag_ids = tags.map(&:id)
|
tag_ids = tags.map(&:id)
|
||||||
@@ -521,7 +504,7 @@ class PostsController < ApplicationController
|
|||||||
sections = sections_by_tag_id.fetch(tag_id, [])
|
sections = sections_by_tag_id.fetch(tag_id, [])
|
||||||
|
|
||||||
if path.include?(tag_id)
|
if path.include?(tag_id)
|
||||||
return TagRepr.inline(tag, language_code:).merge(children: [], sections:)
|
return TagRepr.inline(tag).merge(children: [], sections:)
|
||||||
end
|
end
|
||||||
|
|
||||||
if memo.key?(tag_id)
|
if memo.key?(tag_id)
|
||||||
@@ -533,7 +516,7 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
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
|
end
|
||||||
|
|
||||||
internal_tags = root_ids.filter_map { |id| build_node.call(id, []) }
|
internal_tags = root_ids.filter_map { |id| build_node.call(id, []) }
|
||||||
@@ -689,45 +672,39 @@ class PostsController < ApplicationController
|
|||||||
version.tags_json
|
version.tags_json
|
||||||
.select { _1.key?('tag_id') }
|
.select { _1.key?('tag_id') }
|
||||||
.map { Post.tag_snapshot_literal(_1) }
|
.map { Post.tag_snapshot_literal(_1) }
|
||||||
.compact
|
|
||||||
.sort
|
.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_snapshot_from_record post, locale
|
def post_snapshot_from_record post
|
||||||
{ title: post.title,
|
{ title: post.title,
|
||||||
video_ms: post.video_ms,
|
video_ms: post.video_ms,
|
||||||
original_created_from: snapshot_time(post.original_created_from),
|
original_created_from: snapshot_time(post.original_created_from),
|
||||||
original_created_before: snapshot_time(post.original_created_before),
|
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) }
|
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def editable_tag_names_from_post post, language_code
|
def editable_tag_names_from_post post
|
||||||
post
|
post
|
||||||
.post_tags
|
.post_tags
|
||||||
.joins(tag: :tag_names)
|
.joins(tag: :tag_name)
|
||||||
.merge(Tag.where(deprecated_at: nil))
|
.merge(Tag.where(deprecated_at: nil))
|
||||||
.where(tag_names: { language_code:, primary_flg: true })
|
.includes(:sections, tag: :tag_name)
|
||||||
.includes(:sections, tag: :tag_names)
|
|
||||||
.order('tag_names.name')
|
.order('tag_names.name')
|
||||||
.map do |post_tag|
|
.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)
|
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||||
|
|
||||||
next name if sections.empty?
|
next name if sections.empty?
|
||||||
|
|
||||||
"#{ name }#{ sections.map { Post.section_literal(_1) }.join }"
|
"#{ name }#{ sections.map { Post.section_literal(_1) }.join }"
|
||||||
end.compact.sort
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:,
|
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
||||||
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
||||||
validate_original_created_values!(original_created_from, original_created_before)
|
validate_original_created_values!(original_created_from, original_created_before)
|
||||||
|
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||||
Tag.normalise_tags!(locale, tag_names,
|
|
||||||
with_tagme: false,
|
|
||||||
with_no_deerjikist: false,
|
|
||||||
deny_deprecated: true,
|
|
||||||
with_sections: true) => { tags:, sections: }
|
with_sections: true) => { tags:, sections: }
|
||||||
|
|
||||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||||
@@ -739,7 +716,7 @@ class PostsController < ApplicationController
|
|||||||
original_created_from: snapshot_time(original_created_from),
|
original_created_from: snapshot_time(original_created_from),
|
||||||
original_created_before: snapshot_time(original_created_before),
|
original_created_before: snapshot_time(original_created_before),
|
||||||
tag_names: tags.uniq(&:id).map { |tag|
|
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,
|
}.sort,
|
||||||
parent_post_ids: parent_post_ids.sort }
|
parent_post_ids: parent_post_ids.sort }
|
||||||
end
|
end
|
||||||
@@ -863,7 +840,7 @@ class PostsController < ApplicationController
|
|||||||
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
|
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_post_snapshot! locale, post, snapshot
|
def apply_post_snapshot! post, snapshot
|
||||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
||||||
|
|
||||||
post.update!(title: snapshot[:title],
|
post.update!(title: snapshot[:title],
|
||||||
@@ -871,12 +848,11 @@ class PostsController < ApplicationController
|
|||||||
original_created_from: snapshot[:original_created_from],
|
original_created_from: snapshot[:original_created_from],
|
||||||
original_created_before: snapshot[:original_created_before])
|
original_created_before: snapshot[:original_created_before])
|
||||||
|
|
||||||
Tag.normalise_tags!(locale, snapshot[:tag_names],
|
Tag.normalise_tags!(snapshot[:tag_names],
|
||||||
with_tagme: false,
|
with_tagme: false,
|
||||||
deny_deprecated: true,
|
deny_deprecated: true,
|
||||||
with_sections: true) => { tags:, sections: }
|
with_sections: true) => { tags:, sections: }
|
||||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user,
|
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user)
|
||||||
language_code: locale.language_code)
|
|
||||||
|
|
||||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
class TagDescriptionsController < ApplicationController
|
|
||||||
def show
|
|
||||||
description = TagDescription.find_by(tag_id: params[:id], language_code: 'ja')
|
|
||||||
return head :not_found unless description
|
|
||||||
|
|
||||||
render json: representation(description)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_by_title
|
|
||||||
tag_name = TagName.find_by(language_code: 'ja', name: params[:name].to_s.strip)
|
|
||||||
description = tag_name&.tag && TagDescription.find_by(tag: tag_name.tag, language_code: 'ja')
|
|
||||||
return head :not_found unless description
|
|
||||||
|
|
||||||
render json: representation(description)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
return head :unauthorized unless current_user
|
|
||||||
return head :forbidden unless current_user.gte_member?
|
|
||||||
|
|
||||||
body = params[:body].to_s
|
|
||||||
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
|
||||||
|
|
||||||
tag = Tag.find_by(id: params[:id])
|
|
||||||
return head :not_found unless tag
|
|
||||||
|
|
||||||
description = TagDescription.find_by(tag:, language_code: 'ja')
|
|
||||||
|
|
||||||
if description
|
|
||||||
TagDescriptionOperation.update!(tag:, body:, updated_by_user: current_user)
|
|
||||||
description.reload
|
|
||||||
render json: representation(description)
|
|
||||||
else
|
|
||||||
TagDescriptionOperation.create!(tag:, body:, created_by_user: current_user)
|
|
||||||
description = TagDescription.find_by!(tag:, language_code: 'ja')
|
|
||||||
render json: representation(description), status: :created
|
|
||||||
end
|
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
|
||||||
render_validation_error e.record
|
|
||||||
rescue ActiveRecord::RecordNotUnique
|
|
||||||
render_record_not_unique
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def representation description
|
|
||||||
description.as_json(only: [
|
|
||||||
:tag_id,
|
|
||||||
:language_code,
|
|
||||||
:body,
|
|
||||||
:version_no,
|
|
||||||
:created_by_user_id,
|
|
||||||
:updated_by_user_id,
|
|
||||||
:created_at,
|
|
||||||
:updated_at])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
class TagGroupsController < ApplicationController
|
|
||||||
def index
|
|
||||||
groups = TagGroup.includes(:created_by_user, :updated_by_user).order(:id)
|
|
||||||
render json: TagGroupRepr.many(groups.to_a)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
|
||||||
group = TagGroup.includes(:created_by_user, :updated_by_user).find_by(id: params[:id])
|
|
||||||
return head :not_found unless group
|
|
||||||
|
|
||||||
render json: TagGroupRepr.one(group)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
return head :unauthorized unless current_user
|
|
||||||
return head :forbidden unless current_user.gte_member?
|
|
||||||
|
|
||||||
group = TagGroupOperation.create!(**write_attributes, actor: current_user)
|
|
||||||
render json: TagGroupRepr.one(group), status: :created
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
return head :unauthorized unless current_user
|
|
||||||
return head :forbidden unless current_user.gte_member?
|
|
||||||
|
|
||||||
group = TagGroup.find_by(id: params[:id])
|
|
||||||
return head :not_found unless group
|
|
||||||
|
|
||||||
TagGroupOperation.update!(group:, **write_attributes, actor: current_user)
|
|
||||||
render json: TagGroupRepr.one(group.reload)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def write_attributes
|
|
||||||
{ name: params[:name], description: params[:description],
|
|
||||||
members: params[:members], placements: params[:placements] }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
class TagVersionsController < ApplicationController
|
class TagVersionsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
locale = resolve_locale!
|
|
||||||
tag_id = params[:id].presence
|
tag_id = params[:id].presence
|
||||||
page = (params[:page].presence || 1).to_i
|
page = (params[:page].presence || 1).to_i
|
||||||
limit = (params[:limit].presence || 20).to_i
|
limit = (params[:limit].presence || 20).to_i
|
||||||
@@ -19,8 +18,7 @@ class TagVersionsController < ApplicationController
|
|||||||
SQL
|
SQL
|
||||||
.select('tag_versions.*', 'prev.name AS prev_name', 'prev.category AS prev_category',
|
.select('tag_versions.*', 'prev.name AS prev_name', 'prev.category AS prev_category',
|
||||||
'prev.deprecated_at AS prev_deprecated_at',
|
'prev.deprecated_at AS prev_deprecated_at',
|
||||||
'prev.aliases AS prev_aliases', 'prev.names AS prev_names',
|
'prev.aliases AS prev_aliases', 'prev.parent_tag_ids AS prev_parent_tag_ids')
|
||||||
'prev.parent_tag_ids AS prev_parent_tag_ids')
|
|
||||||
q = q.where('tag_versions.tag_id = ?', tag_id) if tag_id
|
q = q.where('tag_versions.tag_id = ?', tag_id) if tag_id
|
||||||
|
|
||||||
count = q.except(:select, :order, :limit, :offset).count
|
count = q.except(:select, :order, :limit, :offset).count
|
||||||
@@ -29,20 +27,18 @@ class TagVersionsController < ApplicationController
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
|
|
||||||
render json: { versions: serialise_versions(versions, locale.language_code), count: }
|
render json: { versions: serialise_versions(versions), count: }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def serialise_versions rows, language_code
|
def serialise_versions rows
|
||||||
user_ids = rows.map(&:created_by_user_id).compact.uniq
|
user_ids = rows.map(&:created_by_user_id).compact.uniq
|
||||||
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
|
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
|
||||||
|
|
||||||
rows.map do |row|
|
rows.map do |row|
|
||||||
current_names = names_for_locale(row.names, language_code)
|
cur_aliases = split_values(row.aliases)
|
||||||
previous_names = names_for_locale(row.attributes['prev_names'], language_code)
|
prev_aliases = split_values(row.attributes['prev_aliases'])
|
||||||
cur_aliases = current_names.fetch('aliases', []).map { _1.fetch('name') }
|
|
||||||
prev_aliases = previous_names.fetch('aliases', []).map { _1.fetch('name') }
|
|
||||||
|
|
||||||
cur_parent_tag_ids = split_parent_tag_ids(row.parent_tag_ids)
|
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'])
|
prev_parent_tag_ids = split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])
|
||||||
@@ -51,22 +47,21 @@ class TagVersionsController < ApplicationController
|
|||||||
|
|
||||||
tags_by_id =
|
tags_by_id =
|
||||||
Tag
|
Tag
|
||||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
.includes(:tag_name, :materials, { tag_name: :wiki_page })
|
||||||
.where(id: all_parent_tag_ids)
|
.where(id: all_parent_tag_ids)
|
||||||
.index_by(&:id)
|
.index_by(&:id)
|
||||||
|
|
||||||
parent_tags =
|
parent_tags =
|
||||||
build_version_values(cur_parent_tag_ids, prev_parent_tag_ids, key: :tag_id)
|
build_version_values(cur_parent_tag_ids, prev_parent_tag_ids, key: :tag_id)
|
||||||
.map do |h|
|
.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] }
|
type: h[:type] }
|
||||||
end
|
end
|
||||||
|
|
||||||
{ tag_id: row.tag_id,
|
{ tag_id: row.tag_id,
|
||||||
version_no: row.version_no,
|
version_no: row.version_no,
|
||||||
event_type: row.event_type,
|
event_type: row.event_type,
|
||||||
names: row.names,
|
name: { current: row.name, prev: row.attributes['prev_name'] },
|
||||||
name: { current: current_names['name'], prev: previous_names['name'] },
|
|
||||||
category: { current: row.category, prev: row.attributes['prev_category'] },
|
category: { current: row.category, prev: row.attributes['prev_category'] },
|
||||||
deprecated_at: { current: row.deprecated_at&.iso8601,
|
deprecated_at: { current: row.deprecated_at&.iso8601,
|
||||||
prev: row.attributes['prev_deprecated_at']&.iso8601 },
|
prev: row.attributes['prev_deprecated_at']&.iso8601 },
|
||||||
@@ -96,12 +91,5 @@ class TagVersionsController < ApplicationController
|
|||||||
|
|
||||||
def split_values(values) = values.to_s.split(/\s+/).reject(&:blank?)
|
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) : names
|
|
||||||
values&.fetch(language_code, {}) || {}
|
|
||||||
rescue JSON::ParserError
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
|
|
||||||
def split_parent_tag_ids(values) = split_values(values).map(&:to_i)
|
def split_parent_tag_ids(values) = split_values(values).map(&:to_i)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ require 'set'
|
|||||||
|
|
||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
post_id = params[:post]
|
post_id = params[:post]
|
||||||
|
|
||||||
name = params[:name].presence
|
name = params[:name].presence
|
||||||
@@ -38,14 +36,9 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
tags =
|
tags =
|
||||||
if post_id.present?
|
if post_id.present?
|
||||||
Tag
|
Tag.joins(:posts, :tag_name).where(posts: { id: post_id })
|
||||||
.joins(:posts, :tag_names)
|
|
||||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
|
||||||
posts: { id: post_id })
|
|
||||||
else
|
else
|
||||||
Tag
|
Tag.joins(:tag_name)
|
||||||
.joins(:tag_names)
|
|
||||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
external_tags =
|
external_tags =
|
||||||
@@ -174,10 +167,9 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
tags_by_id =
|
tags_by_id =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
.where(id: tag_ids)
|
||||||
id: tag_ids)
|
|
||||||
.index_by(&:id)
|
.index_by(&:id)
|
||||||
external_tags_by_id =
|
external_tags_by_id =
|
||||||
ExternalTag
|
ExternalTag
|
||||||
@@ -186,7 +178,7 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
render json: { tags: rows.map { |row|
|
render json: { tags: rows.map { |row|
|
||||||
if row['source'] == 'tag'
|
if row['source'] == 'tag'
|
||||||
TagRepr.base(tags_by_id.fetch(row['id']), language_code: locale.language_code)
|
TagRepr.base(tags_by_id.fetch(row['id']))
|
||||||
else
|
else
|
||||||
external_tag_json(external_tags_by_id.fetch(row['id']))
|
external_tag_json(external_tags_by_id.fetch(row['id']))
|
||||||
end
|
end
|
||||||
@@ -198,8 +190,7 @@ class TagsController < ApplicationController
|
|||||||
parent_tag_id = nil if parent_tag_id <= 0
|
parent_tag_id = nil if parent_tag_id <= 0
|
||||||
material_filter = material_filter_param(default: 'any')
|
material_filter = material_filter_param(default: 'any')
|
||||||
|
|
||||||
locale = resolve_locale!
|
graph = build_with_depth_graph(material_filter)
|
||||||
graph = build_with_depth_graph(material_filter, locale.language_code)
|
|
||||||
|
|
||||||
tag_ids =
|
tag_ids =
|
||||||
if parent_tag_id
|
if parent_tag_id
|
||||||
@@ -214,8 +205,6 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def autocomplete
|
def autocomplete
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
q = params[:q].to_s.strip.sub(/\Anot:/i, '')
|
q = params[:q].to_s.strip.sub(/\Anot:/i, '')
|
||||||
prefix = "#{ ActiveRecord::Base.sanitize_sql_like(q) }%"
|
prefix = "#{ ActiveRecord::Base.sanitize_sql_like(q) }%"
|
||||||
|
|
||||||
@@ -224,36 +213,35 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
alias_rows =
|
alias_rows =
|
||||||
TagName
|
TagName
|
||||||
.where(language_code: locale.language_code, primary_flg: false)
|
|
||||||
.where('name LIKE ?', prefix)
|
.where('name LIKE ?', prefix)
|
||||||
.pluck(:tag_id, :name)
|
.where.not(canonical_id: nil)
|
||||||
|
.pluck(:canonical_id, :name)
|
||||||
|
|
||||||
matched_alias_by_tag_id = { }
|
matched_alias_by_tag_name_id = { }
|
||||||
tag_ids = []
|
canonical_ids = []
|
||||||
alias_rows.each do |tag_id, alias_name|
|
|
||||||
tag_ids << tag_id
|
alias_rows.each do |canonical_id, alias_name|
|
||||||
matched_alias_by_tag_id[tag_id] ||= alias_name
|
canonical_ids << canonical_id
|
||||||
|
matched_alias_by_tag_name_id[canonical_id] ||= alias_name
|
||||||
end
|
end
|
||||||
|
|
||||||
base =
|
base =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
.where(deprecated_at: nil)
|
||||||
deprecated_at: nil)
|
|
||||||
base = base.where('tags.post_count > 0') if present_only
|
base = base.where('tags.post_count > 0') if present_only
|
||||||
|
|
||||||
canonical_hit = base.where('tag_names.name LIKE ?', prefix)
|
canonical_hit = base.where('tag_names.name LIKE ?', prefix)
|
||||||
|
|
||||||
internal_tags = canonical_hit.or(base.where(id: tag_ids.uniq))
|
internal_tags = canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq))
|
||||||
|
|
||||||
internal_rows =
|
internal_rows =
|
||||||
internal_tags
|
internal_tags
|
||||||
.order(Arel.sql('tags.post_count DESC, tag_names.name'))
|
.order(Arel.sql('tags.post_count DESC, tag_names.name'))
|
||||||
.limit(20)
|
.limit(20)
|
||||||
.map { |tag|
|
.map { |tag|
|
||||||
TagRepr.base(tag, language_code: locale.language_code)
|
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
|
||||||
.merge(matched_alias: matched_alias_by_tag_id[tag.id])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return render json: internal_rows unless with_nico
|
return render json: internal_rows unless with_nico
|
||||||
@@ -270,22 +258,19 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
rows =
|
rows =
|
||||||
(internal_rows + external_rows)
|
(internal_rows + external_rows)
|
||||||
.sort_by { |row| [-row['post_count'], row['name'] || row[:name]] }
|
.sort_by { |row| [-row['post_count'], row['name']] }
|
||||||
.first(20)
|
.first(20)
|
||||||
|
|
||||||
render json: rows
|
render json: rows
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
tag =
|
tag =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||||
.find_by(id: params[:id],
|
.find_by(id: params[:id])
|
||||||
tag_names: { language_code: locale.language_code, primary_flg: true })
|
return render json: TagRepr.base(tag) if tag
|
||||||
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
|
|
||||||
|
|
||||||
external_tag = ExternalTag.find_by(id: params[:id])
|
external_tag = ExternalTag.find_by(id: params[:id])
|
||||||
return render json: ExternalTagRepr.base(external_tag) if external_tag
|
return render json: ExternalTagRepr.base(external_tag) if external_tag
|
||||||
@@ -297,14 +282,12 @@ class TagsController < ApplicationController
|
|||||||
name = params[:name].to_s.strip
|
name = params[:name].to_s.strip
|
||||||
return render_bad_request('name は必須です.') if name.blank?
|
return render_bad_request('name は必須です.') if name.blank?
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
tag =
|
tag =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||||
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
|
.find_by(tag_names: { name: })
|
||||||
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
|
return render json: TagRepr.base(tag) if tag
|
||||||
|
|
||||||
platform, external_name = name.split(':', 2)
|
platform, external_name = name.split(':', 2)
|
||||||
return head :not_found unless external_name
|
return head :not_found unless external_name
|
||||||
@@ -316,52 +299,35 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def deerjikists
|
def deerjikists
|
||||||
locale = resolve_locale!
|
tag = Tag.joins(:tag_name)
|
||||||
|
.includes(:tag_name, tag_name: :wiki_page)
|
||||||
tag =
|
.find_by(id: params[:id])
|
||||||
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 })
|
|
||||||
return head :not_found unless tag
|
return head :not_found unless tag
|
||||||
|
|
||||||
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
|
render json: { tag: TagRepr.base(tag),
|
||||||
deerjikists: DeerjikistRepr.many(
|
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
|
||||||
tag.deerjikists,
|
|
||||||
language_code: locale.language_code) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def deerjikists_by_name
|
def deerjikists_by_name
|
||||||
name = params[:name].to_s.strip
|
name = params[:name].to_s.strip
|
||||||
return render_bad_request('name は必須です.') if name.blank?
|
return render_bad_request('name は必須です.') if name.blank?
|
||||||
|
|
||||||
locale = resolve_locale!
|
tag = Tag.joins(:tag_name)
|
||||||
|
.includes(:tag_name, tag_name: :wiki_page)
|
||||||
tag =
|
.find_by(tag_names: { name: })
|
||||||
Tag
|
|
||||||
.joins(:tag_names)
|
|
||||||
.includes(:tag_names, tag_names: :wiki_page)
|
|
||||||
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
|
|
||||||
return head :not_found unless tag
|
return head :not_found unless tag
|
||||||
|
|
||||||
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
|
render json: { tag: TagRepr.base(tag),
|
||||||
deerjikists: DeerjikistRepr.many(
|
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
|
||||||
tag.deerjikists,
|
|
||||||
language_code: locale.language_code) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_deerjikists
|
def update_deerjikists
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
locale = resolve_locale!
|
tag = Tag.joins(:tag_name)
|
||||||
|
.includes(:tag_name, tag_name: :wiki_page)
|
||||||
tag =
|
.find_by(id: params[:id])
|
||||||
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 })
|
|
||||||
return head :not_found unless tag
|
return head :not_found unless tag
|
||||||
|
|
||||||
rows = normalise_deerjikist_rows(tag)
|
rows = normalise_deerjikist_rows(tag)
|
||||||
@@ -380,7 +346,7 @@ class TagsController < ApplicationController
|
|||||||
row_indexes_by_key.key?([deerjikist.platform, deerjikist.code])
|
row_indexes_by_key.key?([deerjikist.platform, deerjikist.code])
|
||||||
}
|
}
|
||||||
|
|
||||||
render_deerjikist_conflicts(locale, requested_deerjikists, row_indexes_by_key, tag)
|
render_deerjikist_conflicts(requested_deerjikists, row_indexes_by_key, tag)
|
||||||
raise ActiveRecord::Rollback if performed?
|
raise ActiveRecord::Rollback if performed?
|
||||||
|
|
||||||
requested_keys_set = requested_keys.to_set
|
requested_keys_set = requested_keys.to_set
|
||||||
@@ -401,7 +367,7 @@ class TagsController < ApplicationController
|
|||||||
render_deerjikist_form_record_invalid(deerjikist, row_index) unless deerjikist.save
|
render_deerjikist_form_record_invalid(deerjikist, row_index) unless deerjikist.save
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
conflicts = lock_deerjikists_for_tag_update(tag.id, [[platform, code]])
|
conflicts = lock_deerjikists_for_tag_update(tag.id, [[platform, code]])
|
||||||
render_deerjikist_conflicts(locale, conflicts, row_indexes_by_key, tag)
|
render_deerjikist_conflicts(conflicts, row_indexes_by_key, tag)
|
||||||
end
|
end
|
||||||
raise ActiveRecord::Rollback if performed?
|
raise ActiveRecord::Rollback if performed?
|
||||||
end
|
end
|
||||||
@@ -409,28 +375,22 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
return if performed?
|
return if performed?
|
||||||
|
|
||||||
render json: DeerjikistRepr.many(tag.reload.deerjikists,
|
render json: DeerjikistRepr.many(tag.reload.deerjikists)
|
||||||
language_code: locale.language_code)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def materials_by_name
|
def materials_by_name
|
||||||
name = params[:name].to_s.strip
|
name = params[:name].to_s.strip
|
||||||
return render_bad_request('name は必須です.') if name.blank?
|
return render_bad_request('name は必須です.') if name.blank?
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
material_filter = material_filter_param(default: 'any')
|
material_filter = material_filter_param(default: 'any')
|
||||||
|
|
||||||
tag =
|
tag = Tag.joins(:tag_name)
|
||||||
Tag
|
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||||
.joins(:tag_names)
|
.find_by(tag_names: { name: })
|
||||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
|
||||||
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
|
|
||||||
return head :not_found unless tag
|
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
|
end
|
||||||
|
|
||||||
def update_all
|
def update_all
|
||||||
@@ -446,9 +406,7 @@ class TagsController < ApplicationController
|
|||||||
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
|
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
|
||||||
return render_unprocessable_entity '廃止状態は必須です.', field: :deprecated unless params.key?(:deprecated)
|
return render_unprocessable_entity '廃止状態は必須です.', field: :deprecated unless params.key?(:deprecated)
|
||||||
|
|
||||||
locale = resolve_locale!
|
return unless validate_tag_rename(tag, name)
|
||||||
|
|
||||||
return unless validate_tag_rename(locale, tag, name)
|
|
||||||
|
|
||||||
alias_names = params[:aliases].to_s.split.uniq
|
alias_names = params[:aliases].to_s.split.uniq
|
||||||
parent_names = params[:parent_tags].to_s.split.uniq
|
parent_names = params[:parent_tags].to_s.split.uniq
|
||||||
@@ -459,24 +417,24 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
language_code: locale.language_code)
|
|
||||||
|
|
||||||
old_name = tag.name(locale.language_code)
|
old_name = tag.name
|
||||||
name_changed = name != old_name
|
name_changed = name != old_name
|
||||||
|
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||||
|
|
||||||
if tag.deprecated? == deprecated
|
if tag.deprecated? == deprecated
|
||||||
tag.update!(category:)
|
tag.update!(category:)
|
||||||
else
|
else
|
||||||
tag.update!(category:, deprecated_at: deprecated ? Time.current : nil)
|
tag.update!(category:, deprecated_at: deprecated ? Time.current : nil)
|
||||||
end
|
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)
|
alias_names.delete(name)
|
||||||
|
|
||||||
update_aliases!(locale, tag, alias_names)
|
update_aliases!(tag, alias_names)
|
||||||
update_parent_tags!(locale, tag, parent_names)
|
update_parent_tags!(tag, parent_names)
|
||||||
|
|
||||||
tag.reload
|
tag.reload
|
||||||
|
|
||||||
@@ -484,18 +442,17 @@ class TagsController < ApplicationController
|
|||||||
tag,
|
tag,
|
||||||
event_type: :update,
|
event_type: :update,
|
||||||
created_by_user: current_user,
|
created_by_user: current_user,
|
||||||
language_code: locale.language_code)
|
name_changed:,
|
||||||
|
wiki_page:)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
render json: TagRepr.base(tag.reload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
locale = resolve_locale!
|
|
||||||
|
|
||||||
name = params[:name].presence
|
name = params[:name].presence
|
||||||
category = params[:category].presence
|
category = params[:category].presence
|
||||||
deprecated_given = params.key?(:deprecated)
|
deprecated_given = params.key?(:deprecated)
|
||||||
@@ -503,20 +460,20 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
tag = Tag.find(params[:id])
|
tag = Tag.find(params[:id])
|
||||||
|
|
||||||
return unless validate_tag_rename(locale, tag, name)
|
return unless validate_tag_rename(tag, name)
|
||||||
|
|
||||||
if category.present? && category == 'nico'
|
if category.present? && category == 'nico'
|
||||||
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
||||||
end
|
end
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
language_code: locale.language_code)
|
|
||||||
|
|
||||||
old_name = tag.name(locale.language_code)
|
old_name = tag.name
|
||||||
name_changed = name.present? && name != old_name
|
name_changed = name.present? && name != old_name
|
||||||
|
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?
|
tag.update!(category:) if category.present?
|
||||||
if deprecated_given && tag.deprecated? != deprecated
|
if deprecated_given && tag.deprecated? != deprecated
|
||||||
tag.update!(deprecated_at: deprecated ? Time.current : nil)
|
tag.update!(deprecated_at: deprecated ? Time.current : nil)
|
||||||
@@ -528,10 +485,11 @@ class TagsController < ApplicationController
|
|||||||
tag,
|
tag,
|
||||||
event_type: :update,
|
event_type: :update,
|
||||||
created_by_user: current_user,
|
created_by_user: current_user,
|
||||||
language_code: locale.language_code)
|
name_changed:,
|
||||||
|
wiki_page:)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
render json: TagRepr.base(tag.reload)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -543,7 +501,7 @@ class TagsController < ApplicationController
|
|||||||
value
|
value
|
||||||
end
|
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] = [] }
|
children_by_parent_id = Hash.new { |h, k| h[k] = [] }
|
||||||
parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] }
|
parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] }
|
||||||
|
|
||||||
@@ -558,11 +516,8 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
material_tag_ids = Material.unscoped.kept.where.not(tag_id: nil).distinct.pluck(:tag_id).to_set
|
material_tag_ids = Material.unscoped.kept.where.not(tag_id: nil).distinct.pluck(:tag_id).to_set
|
||||||
|
|
||||||
tags_by_id =
|
tags_by_id = Tag.joins(:tag_name)
|
||||||
Tag
|
.where(id: tag_ids)
|
||||||
.joins(:tag_names)
|
|
||||||
.where(id: tag_ids,
|
|
||||||
tag_names: { language_code:, primary_flg: true })
|
|
||||||
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
||||||
.each_with_object({ }) do |(id, name, category, deprecated_at), h|
|
.each_with_object({ }) do |(id, name, category, deprecated_at), h|
|
||||||
h[id] = { name:, category:, deprecated: deprecated_at.present?,
|
h[id] = { name:, category:, deprecated: deprecated_at.present?,
|
||||||
@@ -696,67 +651,61 @@ class TagsController < ApplicationController
|
|||||||
children: [] }
|
children: [] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_tag_children tag, graph: nil, language_code: 'ja'
|
def build_tag_children tag, graph: nil
|
||||||
material = tag.materials.first
|
material = tag.materials.first
|
||||||
tag_graph = graph && graph[:tags_by_id][tag.id]
|
tag_graph = graph && graph[:tags_by_id][tag.id]
|
||||||
material =
|
material =
|
||||||
nil if tag_graph && !with_depth_visible_tag?(tag_graph, graph[:material_filter])
|
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
|
if graph
|
||||||
children = children.filter { |child_tag| visible_subtree?(child_tag.id, graph) }
|
children = children.filter { |child_tag| visible_subtree?(child_tag.id, graph) }
|
||||||
end
|
end
|
||||||
|
|
||||||
TagRepr.base(tag, language_code:).merge(
|
TagRepr.base(tag).merge(
|
||||||
children: children.map { build_tag_children(_1, graph:, language_code:) },
|
children: children.map { build_tag_children(_1, graph:) },
|
||||||
has_material: tag_graph ? tag_graph[:has_material] : material.present?,
|
has_material: tag_graph ? tag_graph[:has_material] : material.present?,
|
||||||
material: material && MaterialRepr.base(
|
material: material && MaterialRepr.base(material, host: request.base_url))
|
||||||
material,
|
|
||||||
host: request.base_url,
|
|
||||||
language_code:))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def record_tag_version! tag, event_type:, created_by_user:, language_code: 'ja'
|
def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil
|
||||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
|
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||||
|
|
||||||
|
return unless name_changed
|
||||||
|
|
||||||
|
wiki_page ||= tag.tag_name.wiki_page
|
||||||
|
return unless wiki_page&.wiki_versions&.exists?
|
||||||
|
|
||||||
|
WikiVersionRecorder.record!(
|
||||||
|
page: wiki_page,
|
||||||
|
event_type: :update,
|
||||||
|
created_by_user:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_tag_rename locale, tag, name
|
def validate_tag_rename tag, name
|
||||||
return true if name.blank? || name == tag.name(locale.language_code)
|
return true if name.blank? || name == tag.name
|
||||||
|
|
||||||
system_tag_names = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ']
|
if tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico])
|
||||||
if TagName.exists?(tag_id: tag.id,
|
|
||||||
language_code: 'ja',
|
|
||||||
primary_flg: true,
|
|
||||||
name: system_tag_names)
|
|
||||||
render_unprocessable_entity 'システム・タグの名称は変更できません.', field: :name
|
render_unprocessable_entity 'システム・タグの名称は変更できません.', field: :name
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
target_tag_name = TagName.find_by(name:)
|
||||||
return true unless target_tag_name&.primary_flg
|
return true if target_tag_name.nil?
|
||||||
|
return true if target_tag_name.canonical_id?
|
||||||
|
|
||||||
render_unprocessable_entity 'その名前は既に使はれてゐます.', field: :name
|
render_unprocessable_entity 'その名前は既に使はれてゐます.', field: :name
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def rename_tag_name! locale, tag, name
|
def rename_tag_name! tag, name
|
||||||
return if name == tag.name(locale.language_code)
|
return if name == tag.name
|
||||||
|
|
||||||
current_tag_name = tag.tag_name(locale.language_code)
|
current_tag_name = tag.tag_name
|
||||||
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
target_tag_name = TagName.find_by(name:)
|
||||||
|
|
||||||
unless target_tag_name
|
if target_tag_name.nil?
|
||||||
if current_tag_name
|
current_tag_name.update!(name:)
|
||||||
current_tag_name.update!(name:, auto_generated: false)
|
|
||||||
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
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -764,95 +713,71 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
|
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
|
if old_owner_tag && old_owner_tag != tag
|
||||||
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
|
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
current_tag_name.wiki_page&.update!(tag_name_id: promoted_tag_name.id)
|
promoted_tag_name.update!(canonical: nil)
|
||||||
current_tag_name.association(:wiki_page).reset
|
|
||||||
|
|
||||||
current_tag_name.update!(canonical_id: promoted_tag_name.id,
|
TagName.where(canonical_id: current_tag_name.id)
|
||||||
primary_flg: false, auto_generated: false)
|
|
||||||
promoted_tag_name.update!(tag:, canonical_id: nil, primary_flg: true, auto_generated: false)
|
|
||||||
|
|
||||||
TagName.where(tag:,
|
|
||||||
language_code: current_tag_name.language_code,
|
|
||||||
primary_flg: false)
|
|
||||||
.where.not(id: promoted_tag_name.id)
|
.where.not(id: promoted_tag_name.id)
|
||||||
.find_each do |alias_tag_name|
|
.find_each do |alias_tag_name|
|
||||||
alias_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
|
alias_tag_name.update!(canonical: promoted_tag_name)
|
||||||
end
|
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(:wiki_page).reset
|
||||||
current_tag_name.association(:tag).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
|
return unless old_owner_tag && old_owner_tag != tag
|
||||||
|
|
||||||
record_tag_version!(old_owner_tag.reload, event_type: :update,
|
record_tag_version!(old_owner_tag.reload, event_type: :update, created_by_user: current_user)
|
||||||
created_by_user: current_user,
|
|
||||||
language_code: current_tag_name.language_code)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_aliases! locale, tag, alias_names
|
def update_aliases! tag, alias_names
|
||||||
alias_names = alias_names.uniq
|
alias_names = alias_names.uniq
|
||||||
|
|
||||||
affected_tags = [tag]
|
affected_tags = [tag]
|
||||||
|
|
||||||
primary_tag_name = TagName.find_by!(tag:,
|
current_aliases = tag.tag_name.aliases.to_a
|
||||||
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.each do |alias_tag_name|
|
current_aliases.each do |alias_tag_name|
|
||||||
next if alias_names.include?(alias_tag_name.name)
|
next if alias_names.include?(alias_tag_name.name)
|
||||||
|
|
||||||
affected_tags << alias_tag_name.tag
|
affected_tags << alias_tag_name.canonical&.tag
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_names.each do |alias_name|
|
alias_names.each do |alias_name|
|
||||||
alias_tag_name = TagName.find_by(language_code: locale.language_code, name: alias_name)
|
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
||||||
if alias_tag_name&.primary? && alias_tag_name.tag_id && alias_tag_name.tag_id != tag.id
|
affected_tags << alias_tag_name.canonical&.tag
|
||||||
alias_tag_name.errors.add(:name, 'その名前は既に使はれてゐます.')
|
|
||||||
raise ActiveRecord::RecordInvalid, alias_tag_name
|
|
||||||
end
|
|
||||||
affected_tags << alias_tag_name&.tag
|
|
||||||
end
|
end
|
||||||
|
|
||||||
affected_tags.compact.uniq.each do |affected_tag|
|
affected_tags.compact.uniq.each do |affected_tag|
|
||||||
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user,
|
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user)
|
||||||
language_code: locale.language_code)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
current_aliases.each do |alias_tag_name|
|
current_aliases.each do |alias_tag_name|
|
||||||
next if alias_names.include?(alias_tag_name.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
|
end
|
||||||
|
|
||||||
alias_names.each do |alias_name|
|
alias_names.each do |alias_name|
|
||||||
alias_tag_name = TagName.find_or_initialize_by(
|
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
||||||
language_code: locale.language_code,
|
alias_tag_name.update!(canonical: tag.tag_name)
|
||||||
name: alias_name)
|
|
||||||
alias_tag_name.script_code = locale.script_code
|
|
||||||
alias_tag_name.update!(tag:, primary_flg: false, auto_generated: false,
|
|
||||||
canonical_id: primary_tag_name.id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
affected_tags.compact.uniq.each do |affected_tag|
|
affected_tags.compact.uniq.each do |affected_tag|
|
||||||
record_tag_version!(affected_tag, event_type: :update,
|
record_tag_version!(affected_tag, event_type: :update, created_by_user: current_user)
|
||||||
created_by_user: current_user,
|
|
||||||
language_code: locale.language_code)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_parent_tags! locale, tag, parent_names
|
def update_parent_tags! tag, parent_names
|
||||||
parent_tags = Tag.normalise_tags!(locale, parent_names,
|
parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false,
|
||||||
with_tagme: false,
|
|
||||||
with_no_deerjikist: false)
|
with_no_deerjikist: false)
|
||||||
|
|
||||||
old_parent_tags = tag.parents.to_a
|
old_parent_tags = tag.parents.to_a
|
||||||
@@ -926,15 +851,13 @@ class TagsController < ApplicationController
|
|||||||
.to_a
|
.to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_deerjikist_conflicts locale, deerjikists, row_indexes_by_key, tag
|
def render_deerjikist_conflicts deerjikists, row_indexes_by_key, tag
|
||||||
conflicts = deerjikists.filter { |deerjikist| deerjikist.tag_id != tag.id }
|
conflicts = deerjikists.filter { |deerjikist| deerjikist.tag_id != tag.id }
|
||||||
return if conflicts.empty?
|
return if conflicts.empty?
|
||||||
|
|
||||||
tag_names_by_id =
|
tag_names_by_id = Tag
|
||||||
Tag
|
.joins(:tag_name)
|
||||||
.joins(:tag_names)
|
.where(id: conflicts.map(&:tag_id).uniq)
|
||||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
|
||||||
id: conflicts.map(&:tag_id).uniq)
|
|
||||||
.pluck('tags.id', 'tag_names.name')
|
.pluck('tags.id', 'tag_names.name')
|
||||||
.to_h
|
.to_h
|
||||||
fields = { }
|
fields = { }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class TheatreSkipEventsController < ApplicationController
|
|||||||
events =
|
events =
|
||||||
TheatreSkipEvent
|
TheatreSkipEvent
|
||||||
.where(theatre_id: params[:theatre_id])
|
.where(theatre_id: params[:theatre_id])
|
||||||
.includes(:post, tags: :tag_names)
|
.includes(:post, tags: :tag_name)
|
||||||
.order(created_at: :desc)
|
.order(created_at: :desc)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ class TheatreSkipEventsController < ApplicationController
|
|||||||
{ id: event.id,
|
{ id: event.id,
|
||||||
theatre_id: event.theatre_id,
|
theatre_id: event.theatre_id,
|
||||||
post: { id: event.post.id, title: event.post.title, url: event.post.url },
|
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,
|
programme_position: event.programme_position,
|
||||||
created_at: event.created_at }
|
created_at: event.created_at }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,82 +3,46 @@ class WikiPagesController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
title = params[:title].to_s.strip
|
title = params[:title].to_s.strip
|
||||||
descriptions =
|
if title.blank?
|
||||||
TagDescription.where(language_code: 'ja')
|
return render json: WikiPageRepr.base(
|
||||||
.includes(tag: :tag_names)
|
WikiPage.joins(:tag_name).includes(tag_name: :tag))
|
||||||
pages = unmigrated_wiki_pages.includes(tag_name: :tag)
|
|
||||||
|
|
||||||
tag_groups = TagGroup.all
|
|
||||||
if title.present?
|
|
||||||
pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%"
|
|
||||||
descriptions = descriptions.joins(tag: :tag_names)
|
|
||||||
.where(tag_names: { language_code: 'ja', primary_flg: true })
|
|
||||||
.where('tag_names.name LIKE ?', pattern).limit(20)
|
|
||||||
tag_groups = tag_groups.where(
|
|
||||||
'CONCAT(?, name) LIKE ?', TagGroupRepr::PREFIX, pattern).limit(20)
|
|
||||||
pages = pages.where('tag_names.name LIKE ?', pattern).limit(20)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
json = descriptions.map { |description| WikiPageRepr.tag_description(description) }
|
q = WikiPage.joins(:tag_name).includes(tag_name: :tag)
|
||||||
json.concat(tag_groups.map { WikiPageRepr.tag_group(_1) })
|
.where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
|
||||||
json.concat(WikiPageRepr.base(pages))
|
render json: WikiPageRepr.base(q.limit(20))
|
||||||
json = json.first(20) if title.present?
|
|
||||||
render json:
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
tag_description = TagDescription.find_by(id: params[:id])
|
|
||||||
|
|
||||||
return render_wiki_page_by_tag_description tag_description if tag_description
|
|
||||||
|
|
||||||
tag_group = TagGroup.find_by(id: params[:id])
|
|
||||||
return render_tag_group tag_group if tag_group
|
|
||||||
|
|
||||||
page = WikiPage.joins(:tag_name)
|
page = WikiPage.joins(:tag_name)
|
||||||
.includes(tag_name: :tag)
|
.includes(tag_name: :tag)
|
||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
|
|
||||||
render_wiki_page_or_404 page
|
render_wiki_page_or_404 page
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_by_title
|
def show_by_title
|
||||||
title = params[:title].to_s.strip
|
title = params[:title].to_s.strip
|
||||||
|
|
||||||
if title.start_with?(TagGroupRepr::PREFIX)
|
|
||||||
name = title.delete_prefix(TagGroupRepr::PREFIX)
|
|
||||||
tag_group = TagGroup.find_by(name:)
|
|
||||||
return render_tag_group tag_group if tag_group
|
|
||||||
return head :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
tag_description = tag_description_by_title(title)
|
|
||||||
|
|
||||||
return render_wiki_page_by_tag_description tag_description if tag_description
|
|
||||||
|
|
||||||
page = WikiPage.joins(:tag_name)
|
page = WikiPage.joins(:tag_name)
|
||||||
.includes(tag_name: :tag)
|
.includes(tag_name: :tag)
|
||||||
.find_by(tag_name: { name: title })
|
.find_by(tag_name: { name: title })
|
||||||
|
|
||||||
render_wiki_page_or_404 page
|
render_wiki_page_or_404 page
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists
|
def exists
|
||||||
return head :no_content if TagDescription.exists?(id: params[:id])
|
if WikiPage.exists?(id: params[:id])
|
||||||
return head :no_content if TagGroup.exists?(id: params[:id])
|
head :no_content
|
||||||
|
else
|
||||||
head WikiPage.exists?(id: params[:id]) ? :no_content : :not_found
|
head :not_found
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists_by_title
|
def exists_by_title
|
||||||
title = params[:title].to_s.strip
|
title = params[:title].to_s.strip
|
||||||
if title.start_with?(TagGroupRepr::PREFIX)
|
if WikiPage.joins(:tag_name).exists?(tag_names: { name: title })
|
||||||
exists = TagGroup.exists?(name: title.delete_prefix(TagGroupRepr::PREFIX))
|
head :no_content
|
||||||
return head exists ? :no_content : :not_found
|
else
|
||||||
|
head :not_found
|
||||||
end
|
end
|
||||||
return head :no_content if tag_description_by_title(title)
|
|
||||||
|
|
||||||
exists = WikiPage.joins(:tag_name).exists?(tag_names: { name: title })
|
|
||||||
head exists ? :no_content : :not_found
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff
|
def diff
|
||||||
@@ -88,49 +52,35 @@ class WikiPagesController < ApplicationController
|
|||||||
from = params[:from].presence
|
from = params[:from].presence
|
||||||
to = params[:to].presence
|
to = params[:to].presence
|
||||||
|
|
||||||
description = TagDescription.find_by(id:)
|
|
||||||
if description
|
|
||||||
versions = description_versions(description)
|
|
||||||
from_rev = from && versions.find(from)
|
|
||||||
to_rev = to ? versions.find(to) : versions.find_by!(version_no: description.version_no)
|
|
||||||
resource = WikiPageRepr.tag_description(description)
|
|
||||||
else
|
|
||||||
tag_group = TagGroup.find_by(id:)
|
|
||||||
if tag_group
|
|
||||||
from_rev = from && group_version(tag_group, from)
|
|
||||||
to_rev = to ? group_version(tag_group, to) : current_group_version(tag_group)
|
|
||||||
return head :not_found if (from && !from_rev) || !to_rev
|
|
||||||
|
|
||||||
from_body = from_rev && render_group_version(from_rev)
|
|
||||||
to_body = render_group_version(to_rev)
|
|
||||||
resource = { 'id' => tag_group.id,
|
|
||||||
'title' => "#{ TagGroupRepr::PREFIX }#{ tag_group.name }",
|
|
||||||
'deprecated_at' => nil }
|
|
||||||
else
|
|
||||||
page = WikiPage.joins(:tag_name).includes(tag_name: :tag).find(id)
|
page = WikiPage.joins(:tag_name).includes(tag_name: :tag).find(id)
|
||||||
|
|
||||||
from_rev = from && page.wiki_revisions.find(from)
|
from_rev = from && page.wiki_revisions.find(from)
|
||||||
to_rev = to ? page.wiki_revisions.find(to) : page.current_revision
|
to_rev = to ? page.wiki_revisions.find(to) : page.current_revision
|
||||||
if ((from_rev && !from_rev.content?) || !to_rev&.content?)
|
if ((from_rev && !(from_rev.content?)) || !(to_rev&.content?))
|
||||||
return render_unprocessable_entity('差分を表示できない版です.')
|
return render_unprocessable_entity('差分を表示できない版です.')
|
||||||
end
|
end
|
||||||
from_body = from_rev&.body
|
|
||||||
to_body = to_rev.body
|
|
||||||
resource = WikiPageRepr.base(page)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
from_body ||= from_rev&.body
|
diffs = Diff::LCS.sdiff(from_rev&.body&.lines || [], to_rev.body.lines)
|
||||||
to_body ||= to_rev.body
|
diff_json = diffs.map { |change|
|
||||||
diff_json = wiki_diff(from_body, to_body)
|
case change.action
|
||||||
|
when ?=
|
||||||
|
{ type: 'context', content: change.old_element }
|
||||||
|
when ?!
|
||||||
|
[{ type: 'removed', content: change.old_element },
|
||||||
|
{ type: 'added', content: change.new_element }]
|
||||||
|
when ?+
|
||||||
|
{ type: 'added', content: change.new_element }
|
||||||
|
when ?-
|
||||||
|
{ type: 'removed', content: change.old_element }
|
||||||
|
end
|
||||||
|
}.flatten.compact
|
||||||
|
|
||||||
render json: { wiki_page_id: resource.fetch('id'),
|
render json: { wiki_page_id: page.id,
|
||||||
title: resource.fetch('title'),
|
title: page.title,
|
||||||
deprecated_at: resource.fetch('deprecated_at'),
|
deprecated_at: page.deprecated_at,
|
||||||
older_revision_id: from_rev&.id,
|
older_revision_id: from_rev&.id,
|
||||||
newer_revision_id: to_rev.id,
|
newer_revision_id: to_rev.id,
|
||||||
diff: diff_json }
|
diff: diff_json }
|
||||||
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
|
|
||||||
render_unprocessable_entity(error.message)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -144,43 +94,20 @@ class WikiPagesController < ApplicationController
|
|||||||
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
||||||
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
||||||
|
|
||||||
if title.start_with?(TagGroupRepr::PREFIX)
|
tag_name = TagName.find_or_create_by!(name: title)
|
||||||
name = title.delete_prefix(TagGroupRepr::PREFIX)
|
|
||||||
parsed = TagGroupMarkdownParser.parse(body:, group: nil)
|
|
||||||
group = TagGroupOperation.create!(name:, **parsed, actor: current_user,
|
|
||||||
require_compatibility: true)
|
|
||||||
version = current_group_version(group)
|
|
||||||
return render_group_representation(TagGroupRepr.one(group), version,
|
|
||||||
body: parsed.fetch(:compatibility_body),
|
|
||||||
status: :created)
|
|
||||||
end
|
|
||||||
|
|
||||||
page = if title.include?(':')
|
page =
|
||||||
ApplicationRecord.transaction do
|
Wiki::Commit.create_content!(
|
||||||
tag_name = TagName.find_or_create_by!(language_code: 'ja', name: title) do |name|
|
tag_name:,
|
||||||
name.script_code = 'Jpan'
|
body:,
|
||||||
name.primary_flg = true
|
created_by_user: current_user,
|
||||||
name.auto_generated = false
|
message:)
|
||||||
end
|
|
||||||
|
|
||||||
Wiki::Commit.create_legacy_content!(
|
|
||||||
tag_name:, body:, created_by_user: current_user, message:)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
tag_name = TagName.find_by(language_code: 'ja', name: title)
|
|
||||||
return render_unprocessable_entity field: :title unless tag_name&.tag
|
|
||||||
|
|
||||||
TagDescriptionOperation.create!(
|
|
||||||
tag: tag_name.tag, body:, created_by_user: current_user, message:)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: WikiPageRepr.base(page), status: :created
|
render json: WikiPageRepr.base(page), status: :created
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
render_validation_error e.record
|
render_validation_error e.record
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
render_record_not_unique
|
render_record_not_unique
|
||||||
rescue TagGroupMarkdownParser::InvalidDocument => error
|
|
||||||
render_unprocessable_entity(error.message)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -193,26 +120,7 @@ class WikiPagesController < ApplicationController
|
|||||||
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
||||||
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
||||||
|
|
||||||
group = TagGroup.find_by(id: params[:id])
|
page = WikiPage.find(params[:id])
|
||||||
if title.start_with?(TagGroupRepr::PREFIX)
|
|
||||||
return head :not_found unless group
|
|
||||||
|
|
||||||
name = title.delete_prefix(TagGroupRepr::PREFIX)
|
|
||||||
parsed = TagGroupMarkdownParser.parse(body:, group:)
|
|
||||||
TagGroupOperation.update!(group:, name:, **parsed, actor: current_user,
|
|
||||||
require_compatibility: true)
|
|
||||||
return head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
page = WikiPage.find_by(id: params[:id])
|
|
||||||
if legacy_group_page?(page)
|
|
||||||
return render_unprocessable_entity field: :title
|
|
||||||
end
|
|
||||||
if group && !legacy_update_identified?(page, title, group)
|
|
||||||
return render_unprocessable_entity field: :title
|
|
||||||
end
|
|
||||||
return head :not_found unless page
|
|
||||||
|
|
||||||
base_revision_id = params[:base_revision_id].presence
|
base_revision_id = params[:base_revision_id].presence
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
@@ -220,7 +128,7 @@ class WikiPagesController < ApplicationController
|
|||||||
|
|
||||||
old_title = page.title
|
old_title = page.title
|
||||||
|
|
||||||
tag = page.tag_name.tag
|
tag = Tag.find_by(tag_name_id: page.tag_name_id)
|
||||||
|
|
||||||
if tag && title != old_title
|
if tag && title != old_title
|
||||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
@@ -229,17 +137,11 @@ class WikiPagesController < ApplicationController
|
|||||||
page.tag_name.update!(name: title) if title != old_title
|
page.tag_name.update!(name: title) if title != old_title
|
||||||
|
|
||||||
message = params[:message].presence
|
message = params[:message].presence
|
||||||
description = TagDescription.find_by(id: page.id)
|
Wiki::Commit.content!(page:,
|
||||||
description_page = description && (
|
body:,
|
||||||
tag&.id == description.tag_id ||
|
created_user: current_user,
|
||||||
(tag.nil? && !old_title.include?(':')))
|
message:,
|
||||||
if description && !description_page
|
base_revision_id:)
|
||||||
Wiki::Commit.legacy_content!(page:, body:, created_user: current_user,
|
|
||||||
message:, base_revision_id:)
|
|
||||||
else
|
|
||||||
Wiki::Commit.content!(page:, body:, created_user: current_user,
|
|
||||||
message:, base_revision_id:)
|
|
||||||
end
|
|
||||||
|
|
||||||
if tag && title != old_title
|
if tag && title != old_title
|
||||||
tag.reload
|
tag.reload
|
||||||
@@ -248,32 +150,20 @@ class WikiPagesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
end
|
||||||
render_validation_error e.record
|
|
||||||
rescue ActiveRecord::RecordNotUnique
|
def search
|
||||||
render_record_not_unique
|
index
|
||||||
rescue TagGroupMarkdownParser::InvalidDocument => error
|
|
||||||
render_unprocessable_entity(error.message)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def changes
|
def changes
|
||||||
id = params[:id].presence
|
id = params[:id].presence
|
||||||
description = id && TagDescription.find_by(id:)
|
q = WikiRevision.joins(wiki_page: :tag_name)
|
||||||
if description
|
|
||||||
return render json: description_changes(TagDescription.where(id: description.id))
|
|
||||||
end
|
|
||||||
|
|
||||||
tag_group = id && TagGroup.find_by(id:)
|
|
||||||
if tag_group
|
|
||||||
return render json: tag_group_changes(TagGroupVersion.where(tag_group_id: tag_group.id))
|
|
||||||
end
|
|
||||||
|
|
||||||
pages = id ? WikiPage.where(id:) : unmigrated_wiki_pages
|
|
||||||
revisions = WikiRevision.joins(wiki_page: :tag_name)
|
|
||||||
.where(wiki_page_id: pages.select(:id))
|
|
||||||
.includes(:created_user, wiki_page: { tag_name: :tag })
|
.includes(:created_user, wiki_page: { tag_name: :tag })
|
||||||
.order(id: :desc).limit(200)
|
.order(id: :desc)
|
||||||
json = revisions.map { |rev|
|
q = q.where(wiki_page_id: id) if id
|
||||||
|
|
||||||
|
render json: q.limit(200).map { |rev|
|
||||||
{ revision_id: rev.id,
|
{ revision_id: rev.id,
|
||||||
pred: rev.base_revision_id,
|
pred: rev.base_revision_id,
|
||||||
succ: nil,
|
succ: nil,
|
||||||
@@ -284,124 +174,11 @@ class WikiPagesController < ApplicationController
|
|||||||
kind: rev.kind,
|
kind: rev.kind,
|
||||||
message: rev.message,
|
message: rev.message,
|
||||||
timestamp: rev.created_at }
|
timestamp: rev.created_at }
|
||||||
}
|
}.compact
|
||||||
unless id
|
|
||||||
json.concat(tag_group_changes(TagGroupVersion.all))
|
|
||||||
json.concat(description_changes(TagDescription.where(language_code: 'ja')))
|
|
||||||
json = json.each_with_index.sort_by do |row, index|
|
|
||||||
[-row.fetch(:timestamp).to_f, index]
|
|
||||||
end.map(&:first)
|
|
||||||
end
|
|
||||||
render json: json.first(200)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def legacy_group_page? page
|
|
||||||
return false unless page
|
|
||||||
return true if page.title.start_with?(TagGroupRepr::PREFIX)
|
|
||||||
|
|
||||||
prefix = ApplicationRecord.sanitize_sql_like(TagGroupRepr::PREFIX)
|
|
||||||
page.wiki_versions.where('title LIKE ?', "#{ prefix }%").exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
def legacy_update_identified? page, title, group
|
|
||||||
return false unless page
|
|
||||||
return true if page.title == title
|
|
||||||
return false if title == group.name
|
|
||||||
|
|
||||||
namespace = page.title.split(':', 2)
|
|
||||||
return true if namespace.size == 2 && title.start_with?("#{ namespace.first }:")
|
|
||||||
|
|
||||||
base = params[:base_revision_id].presence
|
|
||||||
base && page.wiki_revisions.exists?(id: base)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_tag_group group
|
|
||||||
version = if params[:version].present?
|
|
||||||
group_version(group, params[:version])
|
|
||||||
else
|
|
||||||
current_group_version(group)
|
|
||||||
end
|
|
||||||
return head :not_found unless version
|
|
||||||
|
|
||||||
representation = if params[:version].present?
|
|
||||||
TagGroupRepr.historical(version)
|
|
||||||
else
|
|
||||||
TagGroupRepr.one(group)
|
|
||||||
end
|
|
||||||
render_group_representation(representation, version)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_group_version version
|
|
||||||
representation = TagGroupRepr.historical(version)
|
|
||||||
TagGroupMarkdownRenderer.render(representation)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_group_representation representation, version, body: nil, status: :ok
|
|
||||||
body ||= TagGroupMarkdownRenderer.render(representation)
|
|
||||||
versions = TagGroupVersion.where(tag_group_id: version.tag_group_id)
|
|
||||||
pred = versions.find_by(version_no: version.version_no - 1)&.id
|
|
||||||
succ = versions.find_by(version_no: version.version_no + 1)&.id
|
|
||||||
updated_at = version.created_at
|
|
||||||
render json: {
|
|
||||||
'id' => version.tag_group_id,
|
|
||||||
'title' => "#{ TagGroupRepr::PREFIX }#{ representation.fetch('name') }",
|
|
||||||
'deprecated_at' => nil,
|
|
||||||
'body' => body,
|
|
||||||
'revision_id' => version.id,
|
|
||||||
'pred' => pred,
|
|
||||||
'succ' => succ,
|
|
||||||
'updated_at' => updated_at }, status:
|
|
||||||
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
|
|
||||||
render_unprocessable_entity(error.message)
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_group_version group
|
|
||||||
TagGroupVersion.find_by(tag_group_id: group.id, version_no: group.version_no)
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_version group, version_id
|
|
||||||
TagGroupVersion.find_by(tag_group_id: group.id, id: version_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def wiki_diff from_body, to_body
|
|
||||||
diffs = Diff::LCS.sdiff(from_body.to_s.lines, to_body.to_s.lines)
|
|
||||||
diffs.map { |change|
|
|
||||||
case change.action
|
|
||||||
when ?=
|
|
||||||
{ type: 'context', content: change.old_element }
|
|
||||||
when ?!
|
|
||||||
[{ type: 'removed', content: change.old_element },
|
|
||||||
{ type: 'added', content: change.new_element }]
|
|
||||||
when ?+
|
|
||||||
{ type: 'added', content: change.new_element }
|
|
||||||
when ?-
|
|
||||||
{ type: 'removed', content: change.old_element }
|
|
||||||
end
|
|
||||||
}.flatten.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_group_changes versions
|
|
||||||
rows = versions.includes(:created_by_user).order(id: :desc).limit(200).to_a
|
|
||||||
groups = TagGroup.where(id: rows.map(&:tag_group_id).uniq).index_by(&:id)
|
|
||||||
predecessors = TagGroupVersion.where(tag_group_id: groups.keys,
|
|
||||||
version_no: rows.map { _1.version_no - 1 })
|
|
||||||
.pluck(:tag_group_id, :version_no, :id)
|
|
||||||
.to_h { |group_id, number, version_id| [[group_id, number], version_id] }
|
|
||||||
rows.map do |version|
|
|
||||||
group = groups.fetch(version.tag_group_id)
|
|
||||||
user = version.created_by_user
|
|
||||||
{ revision_id: version.id,
|
|
||||||
pred: predecessors[[group.id, version.version_no - 1]],
|
|
||||||
succ: nil,
|
|
||||||
wiki_page: { id: group.id, title: "#{ TagGroupRepr::PREFIX }#{ version.name }",
|
|
||||||
deprecated_at: nil },
|
|
||||||
user: user && { id: user.id, name: user.name },
|
|
||||||
kind: 'content', message: nil, timestamp: version.created_at }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_wiki_page_or_404 page
|
def render_wiki_page_or_404 page
|
||||||
return head :not_found unless page
|
return head :not_found unless page
|
||||||
|
|
||||||
@@ -423,71 +200,6 @@ class WikiPagesController < ApplicationController
|
|||||||
render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:)
|
render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_wiki_page_by_tag_description description
|
|
||||||
versions = description_versions(description)
|
|
||||||
historical = params[:version].present?
|
|
||||||
rev = historical ? versions.find_by(id: params[:version]) :
|
|
||||||
versions.find_by(version_no: description.version_no)
|
|
||||||
return head :not_found unless rev
|
|
||||||
|
|
||||||
pred = versions.find_by(version_no: rev.version_no - 1)&.id
|
|
||||||
succ = versions.find_by(version_no: rev.version_no + 1)&.id
|
|
||||||
body = historical ? rev.body : description.body
|
|
||||||
updated_at = historical ? rev.created_at : description.updated_at
|
|
||||||
render json: WikiPageRepr.tag_description(description).merge(
|
|
||||||
body:, revision_id: rev.id, pred:, succ:, updated_at:)
|
|
||||||
end
|
|
||||||
|
|
||||||
def description_versions description
|
|
||||||
TagDescriptionVersion.where(tag_id: description.tag_id,
|
|
||||||
language_code: description.language_code)
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_description_by_title title
|
|
||||||
TagDescription.joins(tag: :tag_names)
|
|
||||||
.find_by(language_code: 'ja', tag_names: { language_code: 'ja', name: title })
|
|
||||||
end
|
|
||||||
|
|
||||||
def unmigrated_wiki_pages
|
|
||||||
descriptions = TagDescription.where(language_code: 'ja')
|
|
||||||
|
|
||||||
WikiPage
|
|
||||||
.joins(:tag_name)
|
|
||||||
.where.not(id: descriptions.select(:id))
|
|
||||||
.where.not('tag_names.name LIKE ?', "#{ TagGroupRepr::PREFIX }%")
|
|
||||||
.where('tag_names.tag_id IS NULL OR tag_names.tag_id NOT IN (?)',
|
|
||||||
descriptions.select(:tag_id))
|
|
||||||
end
|
|
||||||
|
|
||||||
def description_changes descriptions
|
|
||||||
versions = TagDescriptionVersion.where(language_code: 'ja',
|
|
||||||
tag_id: descriptions.select(:tag_id))
|
|
||||||
.order(id: :desc).limit(200).to_a
|
|
||||||
return [] if versions.empty?
|
|
||||||
|
|
||||||
by_tag = descriptions.where(tag_id: versions.map(&:tag_id))
|
|
||||||
.includes(tag: :tag_names).index_by(&:tag_id)
|
|
||||||
users = User.where(id: versions.map(&:created_by_user_id)).index_by(&:id)
|
|
||||||
predecessors = TagDescriptionVersion.where(
|
|
||||||
language_code: 'ja', tag_id: by_tag.keys,
|
|
||||||
version_no: versions.map { |version| version.version_no - 1 })
|
|
||||||
.pluck(:tag_id, :version_no, :id)
|
|
||||||
.to_h { |tag_id, version_no, id| [[tag_id, version_no], id] }
|
|
||||||
versions.map do |version|
|
|
||||||
description = by_tag.fetch(version.tag_id)
|
|
||||||
user = users[version.created_by_user_id]
|
|
||||||
{ revision_id: version.id,
|
|
||||||
pred: predecessors[[version.tag_id, version.version_no - 1]],
|
|
||||||
succ: nil,
|
|
||||||
wiki_page: { id: description.id, title: description.tag.name('ja'),
|
|
||||||
deprecated_at: description.tag.deprecated_at },
|
|
||||||
user: user && { id: user.id, name: user.name },
|
|
||||||
kind: 'content',
|
|
||||||
message: nil,
|
|
||||||
timestamp: version.created_at }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_revision page
|
def find_revision page
|
||||||
if params[:version].present?
|
if params[:version].present?
|
||||||
page.wiki_revisions.find_by(id: params[:version])
|
page.wiki_revisions.find_by(id: params[:version])
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ class ExternalTag < ApplicationRecord
|
|||||||
|
|
||||||
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
||||||
|
|
||||||
def snapshot_linked_tag_names language_code = 'ja'
|
def snapshot_linked_tag_names
|
||||||
linked_tags
|
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||||
.joins(:tag_names)
|
|
||||||
.where(tag_names: { language_code:, primary_flg: true })
|
|
||||||
.order('tag_names.name')
|
|
||||||
.pluck('tag_names.name')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
class Language < ApplicationRecord
|
|
||||||
has_many :locales, foreign_key: :language_code, primary_key: :code
|
|
||||||
|
|
||||||
enum :code, ja: 'ja'
|
|
||||||
end
|
|
||||||
@@ -1,27 +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
|
|
||||||
|
|
||||||
enum :code, ja: 'ja', en: 'en', fr: 'fr'
|
|
||||||
|
|
||||||
def self.nipponese = Locale.find('ja')
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def generate_tag_names!
|
|
||||||
ApplicationRecord.transaction do
|
|
||||||
tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id)
|
|
||||||
Tag.where.not(id: tag_ids).find_each do
|
|
||||||
TagName.create!(tag_id: _1.id,
|
|
||||||
language_code:,
|
|
||||||
name: TagName.generate_name(self, _1, _1.tag_name('ja')),
|
|
||||||
script_code:,
|
|
||||||
primary_flg: true,
|
|
||||||
auto_generated: true,
|
|
||||||
canonical_id: nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -126,14 +126,13 @@ class Post < ApplicationRecord
|
|||||||
super(options).merge(thumbnail: nil)
|
super(options).merge(thumbnail: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def snapshot_tag_names language_code = 'ja'
|
def snapshot_tag_names
|
||||||
post_tags
|
post_tags
|
||||||
.joins(tag: :tag_names)
|
.joins(tag: :tag_name)
|
||||||
.where(tag_names: { language_code:, primary_flg: true })
|
.includes(:sections, tag: :tag_name)
|
||||||
.includes(:sections, tag: :tag_names)
|
|
||||||
.order('tag_names.name')
|
.order('tag_names.name')
|
||||||
.map do |post_tag|
|
.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)
|
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||||
|
|
||||||
next name if sections.empty?
|
next name if sections.empty?
|
||||||
@@ -153,17 +152,16 @@ class Post < ApplicationRecord
|
|||||||
"#{ tag.fetch('name') }#{ sections.join }"
|
"#{ tag.fetch('name') }#{ sections.join }"
|
||||||
end
|
end
|
||||||
|
|
||||||
def snapshot_tags_json language_code = 'ja'
|
def snapshot_tags_json
|
||||||
tag_snapshots =
|
tag_snapshots =
|
||||||
post_tags
|
post_tags
|
||||||
.joins(tag: :tag_names)
|
.joins(tag: :tag_name)
|
||||||
.where(tag_names: { language_code:, primary_flg: true })
|
.includes(:sections, tag: :tag_name)
|
||||||
.includes(:sections, tag: :tag_names)
|
|
||||||
.order('tags.id')
|
.order('tags.id')
|
||||||
.map { |pt|
|
.map { |pt|
|
||||||
{ 'tag_id' => pt.tag.id,
|
{ 'tag_id' => pt.tag.id,
|
||||||
'version_no' => pt.tag.version_no,
|
'version_no' => pt.tag.version_no,
|
||||||
'name' => pt.tag.name(language_code),
|
'name' => pt.tag.name,
|
||||||
'category' => pt.tag.category,
|
'category' => pt.tag.category,
|
||||||
'sections' => pt.sections.sort_by(&:begin_ms).map {
|
'sections' => pt.sections.sort_by(&:begin_ms).map {
|
||||||
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
|
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
class Region < ApplicationRecord
|
|
||||||
;
|
|
||||||
end
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class Script < ApplicationRecord
|
|
||||||
has_many :locales, foreign_key: :script_code, primary_key: :code
|
|
||||||
|
|
||||||
enum :code, Jpan: 'Jpan'
|
|
||||||
end
|
|
||||||
+59
-111
@@ -50,10 +50,14 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
has_many :tag_versions
|
has_many :tag_versions
|
||||||
|
|
||||||
has_many :tag_names
|
belongs_to :tag_name
|
||||||
|
delegate :wiki_page, to: :tag_name
|
||||||
|
|
||||||
attribute :version_no, :integer, default: 1
|
attribute :version_no, :integer, default: 1
|
||||||
|
|
||||||
|
delegate :name, to: :tag_name, allow_nil: true
|
||||||
|
validates :tag_name, presence: true
|
||||||
|
|
||||||
enum :category, deerjikist: 'deerjikist',
|
enum :category, deerjikist: 'deerjikist',
|
||||||
meme: 'meme',
|
meme: 'meme',
|
||||||
character: 'character',
|
character: 'character',
|
||||||
@@ -63,8 +67,9 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
validates :category, presence: true, inclusion: { in: Tag.categories.keys }
|
validates :category, presence: true, inclusion: { in: Tag.categories.keys }
|
||||||
|
|
||||||
|
validate :tag_name_mustnt_start_with_nico
|
||||||
|
validate :tag_name_must_be_canonical
|
||||||
validate :category_must_be_deerjikist_with_deerjikists
|
validate :category_must_be_deerjikist_with_deerjikists
|
||||||
validate :tag_names_must_be_valid
|
|
||||||
|
|
||||||
CATEGORY_PREFIXES = {
|
CATEGORY_PREFIXES = {
|
||||||
'general:' => :general,
|
'general:' => :general,
|
||||||
@@ -78,20 +83,8 @@ class Tag < ApplicationRecord
|
|||||||
'mtr:' => :material,
|
'mtr:' => :material,
|
||||||
'meta:' => :meta }.freeze
|
'meta:' => :meta }.freeze
|
||||||
|
|
||||||
SYSTEM_TAG_NAMES = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ', 'YouTube'].freeze
|
def name= val
|
||||||
|
(self.tag_name ||= build_tag_name).name = val
|
||||||
def tag_name language_code
|
|
||||||
if tag_names.loaded?
|
|
||||||
tag_names.find { _1.language_code == language_code && _1.primary_flg }
|
|
||||||
else
|
|
||||||
tag_names.find_by(language_code:, primary_flg: true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
def name(language_code) = tag_name(language_code)&.name
|
|
||||||
def wiki_page = tag_name('ja')&.wiki_page
|
|
||||||
|
|
||||||
def system?
|
|
||||||
tag_name('ja')&.name.in?(SYSTEM_TAG_NAMES)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def deprecated? = deprecated_at?
|
def deprecated? = deprecated_at?
|
||||||
@@ -102,31 +95,14 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||||
|
|
||||||
def snapshot_aliases(language_code)
|
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
|
||||||
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
|
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
|
||||||
end
|
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 snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
def self.normalise_tags! tag_names, with_tagme: true,
|
||||||
|
|
||||||
def snapshot_linked_tag_names language_code
|
|
||||||
linked_tags
|
|
||||||
.joins(:tag_names)
|
|
||||||
.where(tag_names: { language_code:, primary_flg: true })
|
|
||||||
.order('tag_names.name')
|
|
||||||
.pluck('tag_names.name')
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta)
|
|
||||||
def bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
|
|
||||||
def no_deerjikist =
|
|
||||||
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
|
|
||||||
def video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
|
|
||||||
def niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
|
|
||||||
def youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
|
|
||||||
|
|
||||||
def normalise_tags! locale, tag_names,
|
|
||||||
with_tagme: true,
|
|
||||||
with_no_deerjikist: true,
|
with_no_deerjikist: true,
|
||||||
deny_deprecated: false,
|
deny_deprecated: false,
|
||||||
with_sections: false
|
with_sections: false
|
||||||
@@ -156,11 +132,11 @@ class Tag < ApplicationRecord
|
|||||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||||
end
|
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?
|
if deny_deprecated && tag.deprecated?
|
||||||
raise DeprecatedTagNormalisationError, [tag.name(locale.language_code)]
|
raise DeprecatedTagNormalisationError, [tag.name]
|
||||||
end
|
end
|
||||||
|
|
||||||
tag.update!(category: cat) if cat && tag.category != cat
|
tag.update!(category: cat) if cat && tag.category != cat
|
||||||
@@ -184,7 +160,7 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def expand_parent_tags tags
|
def self.expand_parent_tags tags
|
||||||
return [] if tags.blank?
|
return [] if tags.blank?
|
||||||
|
|
||||||
seen = Set.new
|
seen = Set.new
|
||||||
@@ -207,7 +183,7 @@ class Tag < ApplicationRecord
|
|||||||
(result + tags).uniq { |t| t.id }
|
(result + tags).uniq { |t| t.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalise_section_range! begin_raw:, end_raw:, tag_name:
|
def self.normalise_section_range! begin_raw:, end_raw:, tag_name:
|
||||||
begin_ms = begin_raw.empty? ? 0 : time_to_ms!(begin_raw, tag_name:)
|
begin_ms = begin_raw.empty? ? 0 : time_to_ms!(begin_raw, tag_name:)
|
||||||
end_ms = end_raw.empty? ? nil : time_to_ms!(end_raw, tag_name:)
|
end_ms = end_raw.empty? ? nil : time_to_ms!(end_raw, tag_name:)
|
||||||
|
|
||||||
@@ -219,7 +195,7 @@ class Tag < ApplicationRecord
|
|||||||
[begin_ms, end_ms]
|
[begin_ms, end_ms]
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_section_ranges ranges
|
def self.merge_section_ranges ranges
|
||||||
sorted_ranges = ranges.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
|
sorted_ranges = ranges.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
|
||||||
merged = []
|
merged = []
|
||||||
|
|
||||||
@@ -240,37 +216,24 @@ class Tag < ApplicationRecord
|
|||||||
merged
|
merged
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_section_end left_end_ms, right_end_ms
|
def self.merge_section_end left_end_ms, right_end_ms
|
||||||
return nil if left_end_ms.nil? || right_end_ms.nil?
|
return nil if left_end_ms.nil? || right_end_ms.nil?
|
||||||
|
|
||||||
[left_end_ms, right_end_ms].max
|
[left_end_ms, right_end_ms].max
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_by_tag_name! locale, name, category:
|
def self.find_or_create_by_tag_name! name, category:
|
||||||
language_code = locale.language_code
|
tn = TagName.find_or_create_by!(name: name.to_s.strip)
|
||||||
name = name.to_s.strip
|
tn = tn.canonical if tn.canonical_id?
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
Tag.find_or_create_by!(tag_name_id: tn.id) do |t|
|
||||||
tn = TagName.find_or_create_by!(language_code:, name:) do
|
t.category = category
|
||||||
_1.script_code = locale.script_code
|
|
||||||
_1.primary_flg = true
|
|
||||||
end
|
|
||||||
|
|
||||||
tag = tn.tag
|
|
||||||
next tag if tag
|
|
||||||
|
|
||||||
tag = Tag.create!(tag_name_id: tn.id, category:)
|
|
||||||
tn.update!(tag:, primary_flg: true, canonical_id: nil, auto_generated: false)
|
|
||||||
generate_tag_names!(locale, tag, tn)
|
|
||||||
tag.association(:tag_names).reset
|
|
||||||
|
|
||||||
tag
|
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_tags! target_tag, source_tags, created_by_user: nil
|
def self.merge_tags! target_tag, source_tags, created_by_user: nil
|
||||||
target_tag => Tag
|
target_tag => Tag
|
||||||
|
|
||||||
affected_post_ids = Set.new
|
affected_post_ids = Set.new
|
||||||
@@ -294,15 +257,13 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
source_tag_name = source_tag.tag_name('ja')
|
source_tag_name = source_tag.tag_name
|
||||||
|
|
||||||
if source_tag_name.wiki_page.present?
|
if source_tag_name.wiki_page.present?
|
||||||
raise ActiveRecord::RecordInvalid.new(source_tag_name)
|
raise ActiveRecord::RecordInvalid.new(source_tag_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
|
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.destroy!
|
||||||
|
|
||||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||||
@@ -323,7 +284,35 @@ class Tag < ApplicationRecord
|
|||||||
target_tag.reload
|
target_tag.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
def time_to_ms! str, tag_name:
|
def snapshot_aliases = tag_name.aliases.order(:name).pluck(:name)
|
||||||
|
|
||||||
|
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
||||||
|
|
||||||
|
def snapshot_linked_tag_names
|
||||||
|
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def tag_name_mustnt_start_with_nico
|
||||||
|
if name.to_s.downcase.start_with?('nico:')
|
||||||
|
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_name_must_be_canonical
|
||||||
|
if tag_name&.canonical_id?
|
||||||
|
errors.add :tag_name, 'tag_names へは実体を示す必要があります.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def category_must_be_deerjikist_with_deerjikists
|
||||||
|
if !(deerjikist?) && deerjikists.exists?
|
||||||
|
errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.time_to_ms! str, tag_name:
|
||||||
match =
|
match =
|
||||||
case str
|
case str
|
||||||
when /\A(?<seconds>\d+)(?:\.(?<ms>\d{1,3}))?\z/
|
when /\A(?<seconds>\d+)(?:\.(?<ms>\d{1,3}))?\z/
|
||||||
@@ -348,45 +337,4 @@ class Tag < ApplicationRecord
|
|||||||
|
|
||||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def generate_tag_names! locale, tag, base_tag_name
|
|
||||||
ApplicationRecord.transaction do
|
|
||||||
locales =
|
|
||||||
Locale
|
|
||||||
.where(deprecated_at: nil)
|
|
||||||
.where.not(language_code: locale.language_code)
|
|
||||||
.order(:language_code, :created_at, :script_code)
|
|
||||||
.uniq { _1.language_code }
|
|
||||||
locales.each do |locale|
|
|
||||||
language_code = locale.language_code
|
|
||||||
script_code = locale.script_code
|
|
||||||
TagName.create!(tag_id: tag.id,
|
|
||||||
language_code:,
|
|
||||||
name: TagName.generate_name(locale, tag, base_tag_name),
|
|
||||||
script_code:,
|
|
||||||
primary_flg: true,
|
|
||||||
auto_generated: true,
|
|
||||||
canonical_id: nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def tag_names_must_be_valid
|
|
||||||
tag_names.each do |tag_name|
|
|
||||||
next if tag_name.valid?
|
|
||||||
|
|
||||||
tag_name.errors[:name].each { errors.add :name, _1 }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def category_must_be_deerjikist_with_deerjikists
|
|
||||||
if !(deerjikist?) && deerjikists.exists?
|
|
||||||
errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
class TagDescription < ApplicationRecord
|
|
||||||
belongs_to :tag
|
|
||||||
end
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
class TagDescriptionVersion < ApplicationRecord
|
|
||||||
;
|
|
||||||
end
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
class TagGroup < ApplicationRecord
|
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
|
||||||
belongs_to :updated_by_user, class_name: 'User', optional: true
|
|
||||||
|
|
||||||
has_many :tag_group_tags, dependent: :delete_all
|
|
||||||
has_many :tags, through: :tag_group_tags
|
|
||||||
has_many :tag_group_groups, dependent: :delete_all
|
|
||||||
has_many :child_tag_groups, through: :tag_group_groups
|
|
||||||
has_many :placements, -> { order(:position) },
|
|
||||||
class_name: 'TagGroupPlacement', dependent: :delete_all
|
|
||||||
has_many :versions, -> { order(:version_no) },
|
|
||||||
class_name: 'TagGroupVersion', dependent: :delete_all
|
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
|
|
||||||
validates :version_no, numericality: { only_integer: true, greater_than: 0 }
|
|
||||||
end
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
class TagGroupGroup < ApplicationRecord
|
|
||||||
self.primary_key = [:tag_group_id, :child_tag_group_id]
|
|
||||||
|
|
||||||
belongs_to :tag_group
|
|
||||||
belongs_to :child_tag_group, class_name: 'TagGroup'
|
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
|
||||||
end
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
class TagGroupPlacement < ApplicationRecord
|
|
||||||
self.primary_key = [:tag_group_id, :position]
|
|
||||||
|
|
||||||
belongs_to :tag_group
|
|
||||||
belongs_to :tag, optional: true
|
|
||||||
belongs_to :child_tag_group, class_name: 'TagGroup', optional: true
|
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
|
||||||
belongs_to :updated_by_user, class_name: 'User', optional: true
|
|
||||||
end
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
class TagGroupTag < ApplicationRecord
|
|
||||||
self.primary_key = [:tag_group_id, :tag_id]
|
|
||||||
|
|
||||||
belongs_to :tag_group
|
|
||||||
belongs_to :tag
|
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
|
||||||
end
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
class TagGroupVersion < ApplicationRecord
|
|
||||||
include VersionRecord
|
|
||||||
|
|
||||||
belongs_to :tag_group
|
|
||||||
|
|
||||||
scope :ordered, -> { order(:version_no) }
|
|
||||||
end
|
|
||||||
@@ -1,61 +1,43 @@
|
|||||||
class TagName < ApplicationRecord
|
class TagName < ApplicationRecord
|
||||||
belongs_to :language, foreign_key: :language_code, primary_key: :code
|
has_one :tag
|
||||||
belongs_to :tag, optional: true
|
|
||||||
has_one :wiki_page
|
has_one :wiki_page
|
||||||
|
|
||||||
validates :name, presence: true,
|
belongs_to :canonical, class_name: 'TagName', optional: true
|
||||||
length: { maximum: 255 },
|
has_many :aliases, class_name: 'TagName', foreign_key: :canonical_id
|
||||||
uniqueness: { scope: :language_code }
|
|
||||||
|
|
||||||
|
validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
|
||||||
|
|
||||||
|
validate :canonical_must_be_canonical
|
||||||
validate :alias_name_must_not_have_prefix
|
validate :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_must_be_sanitised
|
||||||
validate :name_mustnt_start_with_nico
|
|
||||||
validate :generated_name_must_be_reserved
|
|
||||||
|
|
||||||
def primary? = primary_flg
|
def self.canonicalise names
|
||||||
|
|
||||||
def self.canonicalise locale, names
|
|
||||||
names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?)
|
names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?)
|
||||||
return [] if names.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|
|
names.map { |name| tns[name]&.canonical&.name || name }.uniq
|
||||||
tag_name = tns[name]
|
|
||||||
if !tag_name || tag_name.primary? || tag_name.tag_id.nil?
|
|
||||||
name
|
|
||||||
else
|
|
||||||
TagName.find_by(language_code: locale.language_code,
|
|
||||||
tag_id: tag_name.tag_id,
|
|
||||||
primary_flg: true)&.name || name
|
|
||||||
end
|
|
||||||
}.uniq
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.generate_name locale, tag, base_tag_name
|
|
||||||
# TODO: 言語ごとの自動命名ロジック完成したら書く.
|
|
||||||
|
|
||||||
"Tag_##{ tag.id }"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generated_name_must_be_reserved
|
def canonical_must_be_canonical
|
||||||
if !auto_generated && name.to_s.start_with?('Tag_#')
|
if canonical&.canonical_id?
|
||||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
errors.add :canonical, 'canonical は実体を示す必要があります.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def alias_name_must_not_have_prefix
|
def alias_name_must_not_have_prefix
|
||||||
if !(primary?) && name.to_s.include?(':')
|
if canonical_id? && name.to_s.include?(':')
|
||||||
errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.'
|
errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def alias_must_not_have_wiki_page
|
def canonical_must_not_be_present_with_tag_or_wiki_page
|
||||||
if !(primary?) && wiki_page
|
if canonical_id? && (tag || wiki_page)
|
||||||
errors.add :primary_flg, 'Wiki 参照がある名前はエーリアスになれません.'
|
errors.add :canonical, 'タグもしくは Wiki の参照がある名前はエーリアスになれません.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,10 +46,4 @@ class TagName < ApplicationRecord
|
|||||||
errors.add :name, '名前に使用できない文字が含まれてゐます.'
|
errors.add :name, '名前に使用できない文字が含まれてゐます.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def name_mustnt_start_with_nico
|
|
||||||
if name.to_s.downcase.start_with?('nico:')
|
|
||||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,30 +15,23 @@ class TagNameSanitisationRule < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def apply!
|
def apply!
|
||||||
Language.find_each do |language|
|
TagName.find_each do |tn|
|
||||||
TagName.where(language:).find_each do |tn|
|
|
||||||
name = sanitise(tn.name)
|
name = sanitise(tn.name)
|
||||||
next if name == tn.name
|
next if name == tn.name
|
||||||
|
|
||||||
TagName.transaction do
|
TagName.transaction do
|
||||||
existing_tn = TagName.find_by(language:, name:)
|
existing_tn = TagName.find_by(name:)
|
||||||
if existing_tn
|
if existing_tn
|
||||||
unless existing_tn.primary_flg
|
existing_tn = existing_tn.canonical || existing_tn
|
||||||
existing_tn = TagName.find_by!(language:,
|
|
||||||
tag_id: existing_tn.tag_id,
|
|
||||||
primary_flg: true)
|
|
||||||
end
|
|
||||||
next if existing_tn.id == tn.id
|
next if existing_tn.id == tn.id
|
||||||
|
|
||||||
existing_tag = existing_tn.tag
|
existing_tag = Tag.find_by(tag_name_id: existing_tn.id)
|
||||||
source_tag = tn.tag
|
source_tag = Tag.find_by(tag_name_id: tn.id)
|
||||||
|
|
||||||
if existing_tag
|
if existing_tag
|
||||||
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
|
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
|
||||||
elsif source_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)
|
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
|
end
|
||||||
tn.destroy!
|
tn.destroy!
|
||||||
|
|
||||||
@@ -50,7 +43,6 @@ class TagNameSanitisationRule < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ class TagVersion < ApplicationRecord
|
|||||||
meta: 'meta' }, validate: true
|
meta: 'meta' }, validate: true
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :names, presence: true
|
|
||||||
validates :category, presence: true
|
validates :category, presence: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,11 +48,8 @@ class WikiPage < ApplicationRecord
|
|||||||
page
|
page
|
||||||
end
|
end
|
||||||
|
|
||||||
def pred_revision_id revision_id
|
def pred_revision_id(revision_id) =
|
||||||
wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id)
|
wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id)
|
||||||
end
|
def succ_revision_id(revision_id) =
|
||||||
|
|
||||||
def succ_revision_id revision_id
|
|
||||||
wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id)
|
wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ module DeerjikistRepr
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def base deerjikist, language_code: 'ja'
|
def base deerjikist
|
||||||
deerjikist.as_json(BASE).merge(
|
deerjikist.as_json(BASE)
|
||||||
tag: deerjikist.tag && TagRepr.base(deerjikist.tag, language_code:))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def many deerjikists, language_code: 'ja'
|
def many deerjikists
|
||||||
deerjikists.map { |d| base(d, language_code:) }
|
deerjikists.map { |d| base(d) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,15 +12,14 @@ module MaterialRepr
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def base material, host:, language_code: 'ja'
|
def base material, host:
|
||||||
material.as_json(BASE).merge(
|
material.as_json(BASE).merge(
|
||||||
tag: material.tag && TagRepr.base(material.tag, language_code:),
|
|
||||||
file: if material.file.attached?
|
file: if material.file.attached?
|
||||||
Rails.application.routes.url_helpers.rails_storage_proxy_url(
|
Rails.application.routes.url_helpers.rails_storage_proxy_url(
|
||||||
material.file, host:)
|
material.file, host:)
|
||||||
end,
|
end,
|
||||||
thumbnail: thumbnail_url(material, host:),
|
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),
|
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
|
||||||
media_kind: media_kind(material),
|
media_kind: media_kind(material),
|
||||||
file_byte_size: material.file_byte_size,
|
file_byte_size: material.file_byte_size,
|
||||||
@@ -28,11 +27,11 @@ module MaterialRepr
|
|||||||
export_items: export_items(material))
|
export_items: export_items(material))
|
||||||
end
|
end
|
||||||
|
|
||||||
def many materials, host:, language_code: 'ja'
|
def many materials, host:
|
||||||
materials.map { |m| base(m, host:, language_code:) }
|
materials.map { |m| base(m, host:) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def list material, host:, language_code: 'ja'
|
def list material, host:
|
||||||
{ id: material.id,
|
{ id: material.id,
|
||||||
version_no: material.version_no,
|
version_no: material.version_no,
|
||||||
url: material.url,
|
url: material.url,
|
||||||
@@ -41,9 +40,9 @@ module MaterialRepr
|
|||||||
source_path: material.source_path,
|
source_path: material.source_path,
|
||||||
source_file_id: material.source_file_id,
|
source_file_id: material.source_file_id,
|
||||||
normalized_source_key: material.normalized_source_key,
|
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: 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),
|
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
|
||||||
media_kind: media_kind(material),
|
media_kind: media_kind(material),
|
||||||
content_type: material.content_type,
|
content_type: material.content_type,
|
||||||
@@ -54,8 +53,8 @@ module MaterialRepr
|
|||||||
export_items: export_items(material) }
|
export_items: export_items(material) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_many materials, host:, language_code: 'ja'
|
def list_many materials, host:
|
||||||
materials.map { |m| list(m, host:, language_code:) }
|
materials.map { |m| list(m, host:) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def export_paths material
|
def export_paths material
|
||||||
@@ -80,8 +79,8 @@ module MaterialRepr
|
|||||||
material.thumbnail, host:)
|
material.thumbnail, host:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def thumbnail_fallback_text material, language_code:
|
def thumbnail_fallback_text material
|
||||||
material.tag&.name(language_code) || material.created_at&.strftime('%Y-%m-%d')
|
material.tag&.name || material.created_at&.strftime('%Y-%m-%d')
|
||||||
end
|
end
|
||||||
|
|
||||||
def thumbnail_fallback_kind material
|
def thumbnail_fallback_kind material
|
||||||
@@ -99,11 +98,11 @@ module MaterialRepr
|
|||||||
'file_other'
|
'file_other'
|
||||||
end
|
end
|
||||||
|
|
||||||
def compact_tag tag, language_code:
|
def compact_tag tag
|
||||||
return nil unless tag
|
return nil unless tag
|
||||||
|
|
||||||
{ id: tag.id,
|
{ id: tag.id,
|
||||||
name: tag.name(language_code),
|
name: tag.name,
|
||||||
category: tag.category,
|
category: tag.category,
|
||||||
deprecated_at: tag.deprecated_at }
|
deprecated_at: tag.deprecated_at }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,23 +17,23 @@ module PostRepr
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def base post, current_user = nil, host: nil, language_code: 'ja'
|
def base post, current_user = nil, host: nil
|
||||||
json =
|
json =
|
||||||
if host.present?
|
if host.present?
|
||||||
common(post, host:)
|
common(post, host:)
|
||||||
else
|
else
|
||||||
common(post)
|
common(post)
|
||||||
end
|
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['uploaded_user'] = post.uploaded_user && UserRepr.base(post.uploaded_user)
|
||||||
json['viewed'] = current_user ? current_user.viewed?(post) : false
|
json['viewed'] = current_user ? current_user.viewed?(post) : false
|
||||||
json
|
json
|
||||||
end
|
end
|
||||||
|
|
||||||
def detail post, current_user = nil, parent_posts: [], child_posts: [],
|
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?
|
if host.present?
|
||||||
base(post, current_user, host:, language_code:).merge(
|
base(post, current_user, host:).merge(
|
||||||
'parent_posts' => cards(parent_posts, host:),
|
'parent_posts' => cards(parent_posts, host:),
|
||||||
'child_posts' => cards(child_posts, host:),
|
'child_posts' => cards(child_posts, host:),
|
||||||
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
|
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
|
||||||
@@ -41,7 +41,7 @@ module PostRepr
|
|||||||
},
|
},
|
||||||
'related' => cards(related, host:))
|
'related' => cards(related, host:))
|
||||||
else
|
else
|
||||||
base(post, current_user, language_code:).merge(
|
base(post, current_user).merge(
|
||||||
'parent_posts' => cards(parent_posts),
|
'parent_posts' => cards(parent_posts),
|
||||||
'child_posts' => cards(child_posts),
|
'child_posts' => cards(child_posts),
|
||||||
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
|
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
|
||||||
@@ -86,14 +86,14 @@ module PostRepr
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_json post, language_code:
|
def tag_json post
|
||||||
internal_tags =
|
internal_tags =
|
||||||
post
|
post
|
||||||
.post_tags
|
.post_tags
|
||||||
.reject { _1.tag.deprecated? }
|
.reject { _1.tag.deprecated? }
|
||||||
.sort_by { TagRepr.name_for(_1.tag, language_code) }
|
.sort_by { _1.tag.name }
|
||||||
.map { |post_tag|
|
.map { |post_tag|
|
||||||
TagRepr.inline(post_tag.tag, language_code:).merge(
|
TagRepr.inline(post_tag.tag).merge(
|
||||||
'children' => [],
|
'children' => [],
|
||||||
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
|
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
module TagGroupRepr
|
|
||||||
PREFIX = 'タググループ:'
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def many groups
|
|
||||||
ids = groups.map(&:id)
|
|
||||||
return [] if ids.empty?
|
|
||||||
|
|
||||||
tag_members = TagGroupTag.where(tag_group_id: ids)
|
|
||||||
.group_by(&:tag_group_id)
|
|
||||||
group_members = TagGroupGroup.where(tag_group_id: ids)
|
|
||||||
.group_by(&:tag_group_id)
|
|
||||||
placements = TagGroupPlacement.where(tag_group_id: ids).order(:position)
|
|
||||||
.group_by(&:tag_group_id)
|
|
||||||
|
|
||||||
target_ids = tag_members.values.flatten.map(&:tag_id) +
|
|
||||||
group_members.values.flatten.map(&:child_tag_group_id) +
|
|
||||||
placements.values.flatten.filter_map(&:tag_id)
|
|
||||||
child_ids = group_members.values.flatten.map(&:child_tag_group_id) +
|
|
||||||
placements.values.flatten.filter_map(&:child_tag_group_id)
|
|
||||||
tags = Tag.select(:id, :category).where(id: target_ids.uniq).index_by(&:id)
|
|
||||||
names = TagName.where(tag_id: target_ids.uniq, language_code: 'ja', primary_flg: true)
|
|
||||||
.pluck(:tag_id, :name).to_h
|
|
||||||
child_groups = TagGroup.where(id: child_ids.uniq).index_by(&:id)
|
|
||||||
|
|
||||||
groups.map do |group|
|
|
||||||
group_json(group,
|
|
||||||
tag_members.fetch(group.id, []),
|
|
||||||
group_members.fetch(group.id, []),
|
|
||||||
placements.fetch(group.id, []), tags, names, child_groups)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def one group
|
|
||||||
many([group]).first
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_group wiki_page
|
|
||||||
{ 'id' => wiki_page.id,
|
|
||||||
'title' => "#{ PREFIX }#{ wiki_page.name }",
|
|
||||||
'deprecated_at' => nil,
|
|
||||||
'created_at' => wiki_page.created_at,
|
|
||||||
'updated_at' => wiki_page.updated_at }
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_version group
|
|
||||||
TagGroupVersion.find_by(tag_group_id: group.id, version_no: group.version_no)
|
|
||||||
end
|
|
||||||
|
|
||||||
def historical version
|
|
||||||
members = Array(version.members)
|
|
||||||
placements = Array(version.placements)
|
|
||||||
tag_references = members.filter_map { |member| member['tag_id'] } +
|
|
||||||
collect_ids(placements, 'tag_id')
|
|
||||||
group_references = members.filter_map { |member| member['tag_group_id'] } +
|
|
||||||
collect_ids(placements, 'tag_group_id')
|
|
||||||
tag_versions = historical_tag_versions(tag_references)
|
|
||||||
group_versions = historical_group_versions(group_references)
|
|
||||||
tag_version_numbers = members.filter_map do |member|
|
|
||||||
next unless member['tag_id']
|
|
||||||
|
|
||||||
[member.fetch('tag_id'), member.fetch('version_no')]
|
|
||||||
end.to_h
|
|
||||||
group_version_numbers = members.filter_map do |member|
|
|
||||||
next unless member['tag_group_id']
|
|
||||||
|
|
||||||
[member.fetch('tag_group_id'), member.fetch('version_no')]
|
|
||||||
end.to_h
|
|
||||||
historical_members = members.map do |member|
|
|
||||||
historical_member(member, tag_versions, group_versions)
|
|
||||||
end
|
|
||||||
historical_placements = placements.map do |node|
|
|
||||||
historical_placement(node, tag_versions, group_versions,
|
|
||||||
tag_version_numbers, group_version_numbers)
|
|
||||||
end
|
|
||||||
{ 'id' => version.tag_group_id,
|
|
||||||
'name' => version.name,
|
|
||||||
'description' => version.description,
|
|
||||||
'version_no' => version.version_no,
|
|
||||||
'members' => historical_members,
|
|
||||||
'placements' => historical_placements,
|
|
||||||
'created_at' => version.created_at,
|
|
||||||
'updated_at' => version.created_at,
|
|
||||||
'created_by_user' => user_json(version.created_by_user),
|
|
||||||
'updated_by_user' => user_json(version.created_by_user) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_json group, tag_members, group_members, rows, tags, names, child_groups
|
|
||||||
tree = placement_tree(rows, tags, names, child_groups)
|
|
||||||
members = tag_members.sort_by(&:tag_id).map do |member|
|
|
||||||
{ 'tag' => tag_json(tags.fetch(member.tag_id), names.fetch(member.tag_id)) }
|
|
||||||
end
|
|
||||||
members.concat(group_members.sort_by(&:child_tag_group_id).map do |member|
|
|
||||||
{ 'tag_group' => group_json_reference(child_groups.fetch(member.child_tag_group_id)) }
|
|
||||||
end)
|
|
||||||
{ 'id' => group.id,
|
|
||||||
'name' => group.name,
|
|
||||||
'description' => group.description,
|
|
||||||
'version_no' => group.version_no,
|
|
||||||
'members' => members,
|
|
||||||
'placements' => tree,
|
|
||||||
'created_at' => group.created_at,
|
|
||||||
'updated_at' => group.updated_at,
|
|
||||||
'created_by_user' => user_json(group.created_by_user),
|
|
||||||
'updated_by_user' => user_json(group.updated_by_user) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_json user
|
|
||||||
UserRepr.base(user) if user
|
|
||||||
end
|
|
||||||
|
|
||||||
def placement_tree rows, tags, names, child_groups
|
|
||||||
by_parent = rows.group_by(&:parent_position)
|
|
||||||
build = lambda do |parent|
|
|
||||||
by_parent.fetch(parent, []).sort_by(&:position).map do |row|
|
|
||||||
node = if row.tag_id
|
|
||||||
{ 'tag' => tag_json(tags.fetch(row.tag_id), names.fetch(row.tag_id)) }
|
|
||||||
elsif row.child_tag_group_id
|
|
||||||
{ 'tag_group' => group_json_reference(child_groups.fetch(row.child_tag_group_id)) }
|
|
||||||
else
|
|
||||||
{ 'section_name' => row.section_name, 'description' => row.description }
|
|
||||||
end
|
|
||||||
node.merge('children' => build.call(row.position))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
build.call(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_json tag, name
|
|
||||||
{ 'id' => tag.id, 'name' => name, 'category' => tag.category }
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_json_reference group
|
|
||||||
{ 'id' => group.id, 'name' => group.name }
|
|
||||||
end
|
|
||||||
|
|
||||||
def collect_ids nodes, key
|
|
||||||
nodes.flat_map do |node|
|
|
||||||
[node[key], *collect_ids(Array(node['children']), key)].compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def historical_tag_versions ids
|
|
||||||
TagVersion.where(tag_id: ids.uniq).to_a.index_by do |version|
|
|
||||||
[version.tag_id, version.version_no]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def historical_group_versions ids
|
|
||||||
TagGroupVersion.where(tag_group_id: ids.uniq).to_a.index_by do |version|
|
|
||||||
[version.tag_group_id, version.version_no]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def historical_member member, tag_versions, group_versions
|
|
||||||
if member['tag_id']
|
|
||||||
id = member.fetch('tag_id')
|
|
||||||
version = tag_versions.fetch([id, member.fetch('version_no')])
|
|
||||||
{ 'tag' => { 'id' => id, 'name' => japanese_version_name(version),
|
|
||||||
'category' => version.category } }
|
|
||||||
else
|
|
||||||
id = member.fetch('tag_group_id')
|
|
||||||
version = group_versions.fetch([id, member.fetch('version_no')])
|
|
||||||
{ 'tag_group' => { 'id' => id, 'name' => version.name } }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def historical_placement(node, tag_versions, group_versions,
|
|
||||||
tag_version_numbers, group_version_numbers)
|
|
||||||
result = if node['tag_id']
|
|
||||||
id = node.fetch('tag_id')
|
|
||||||
version_no = tag_version_numbers.fetch(id)
|
|
||||||
version = tag_versions.fetch([id, version_no])
|
|
||||||
{ 'tag' => { 'id' => id, 'name' => japanese_version_name(version),
|
|
||||||
'category' => version.category } }
|
|
||||||
elsif node['tag_group_id']
|
|
||||||
id = node.fetch('tag_group_id')
|
|
||||||
version_no = group_version_numbers.fetch(id)
|
|
||||||
version = group_versions.fetch([id, version_no])
|
|
||||||
{ 'tag_group' => { 'id' => id, 'name' => version.name } }
|
|
||||||
else
|
|
||||||
{ 'section_name' => node.fetch('section_name'), 'description' => node['description'] }
|
|
||||||
end
|
|
||||||
result.merge('children' => Array(node['children']).map do |child|
|
|
||||||
historical_placement(child, tag_versions, group_versions,
|
|
||||||
tag_version_numbers, group_version_numbers)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def japanese_version_name version
|
|
||||||
version.names.fetch('ja').fetch('name')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -3,23 +3,18 @@
|
|||||||
|
|
||||||
module TagRepr
|
module TagRepr
|
||||||
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at, :deprecated_at],
|
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
|
module_function
|
||||||
|
|
||||||
def base tag, language_code: 'ja'
|
def base tag
|
||||||
tag.as_json(BASE).merge(name: name_for(tag, language_code),
|
tag.as_json(BASE).merge(aliases: tag.snapshot_aliases,
|
||||||
aliases: tag.snapshot_aliases(language_code),
|
parents: tag.parents.map { _1.as_json(BASE) })
|
||||||
parents: tag.parents.map { inline(_1, language_code:) })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def inline tag, language_code: 'ja'
|
def inline tag
|
||||||
tag.as_json(BASE).merge(name: name_for(tag, language_code), aliases: [], parents: [])
|
tag.as_json(BASE).merge(aliases: [], parents: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def many(tags, language_code: 'ja') = tags.map { |tag| base(tag, language_code:) }
|
def many(tags) = tags.map { |t| base(t) }
|
||||||
|
|
||||||
def name_for tag, language_code
|
|
||||||
tag.name(language_code) || (tag.system? ? tag.name('ja') : nil)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,25 +10,6 @@ module WikiPageRepr
|
|||||||
wiki_page.as_json(BASE)
|
wiki_page.as_json(BASE)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_description description
|
|
||||||
tag = description.tag
|
|
||||||
{ 'id' => description.id,
|
|
||||||
'tag_name_id' => tag.tag_name('ja')&.id,
|
|
||||||
'body' => description.body,
|
|
||||||
'created_user_id' => description.created_by_user_id,
|
|
||||||
'updated_user_id' => description.updated_by_user_id,
|
|
||||||
'created_at' => description.created_at,
|
|
||||||
'updated_at' => description.updated_at,
|
|
||||||
'discarded_at' => nil,
|
|
||||||
'version_no' => description.version_no,
|
|
||||||
'title' => tag.name('ja'),
|
|
||||||
'deprecated_at' => tag.deprecated_at }
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_group tag_group
|
|
||||||
TagGroupRepr.tag_group(tag_group)
|
|
||||||
end
|
|
||||||
|
|
||||||
def many wiki_pages
|
def many wiki_pages
|
||||||
wiki_pages.map { |p| base(p) }
|
wiki_pages.map { |p| base(p) }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class MaterialVersionRecorder < VersionRecorder
|
|||||||
{ url: @record.url,
|
{ url: @record.url,
|
||||||
parent: @record.parent,
|
parent: @record.parent,
|
||||||
tag: @record.tag,
|
tag: @record.tag,
|
||||||
tag_name: @record.tag&.name('ja'),
|
tag_name: @record.tag&.name,
|
||||||
tag_category: @record.tag&.category,
|
tag_category: @record.tag&.category,
|
||||||
source_kind: @record.source_kind,
|
source_kind: @record.source_kind,
|
||||||
source_uri: @record.source_uri,
|
source_uri: @record.source_uri,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class PostBulkCreator
|
|||||||
@host = host
|
@host = host
|
||||||
end
|
end
|
||||||
|
|
||||||
def run locale
|
def run
|
||||||
results = Array.new(@posts.length)
|
results = Array.new(@posts.length)
|
||||||
mutex = Mutex.new
|
mutex = Mutex.new
|
||||||
next_index = 0
|
next_index = 0
|
||||||
@@ -27,7 +27,7 @@ class PostBulkCreator
|
|||||||
break if index >= @posts.length
|
break if index >= @posts.length
|
||||||
|
|
||||||
attributes = @posts[index]
|
attributes = @posts[index]
|
||||||
results[index] = create_row(locale, actor, attributes, index)
|
results[index] = create_row(actor, attributes, index)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error(
|
Rails.logger.error(
|
||||||
"post_bulk_creator_worker_failure #{ { error: e.class.name,
|
"post_bulk_creator_worker_failure #{ { error: e.class.name,
|
||||||
@@ -61,12 +61,12 @@ class PostBulkCreator
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_row locale, actor, attributes, index
|
def create_row actor, attributes, index
|
||||||
preflight =
|
preflight =
|
||||||
PostCreatePreflight.new(
|
PostCreatePreflight.new(
|
||||||
attributes: attributes,
|
attributes: attributes,
|
||||||
thumbnail: thumbnail_for(index, attributes),
|
thumbnail: thumbnail_for(index, attributes),
|
||||||
host: @host).run(locale)
|
host: @host).run
|
||||||
if preflight[:existing_post_id].present?
|
if preflight[:existing_post_id].present?
|
||||||
return {
|
return {
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
@@ -76,7 +76,7 @@ class PostBulkCreator
|
|||||||
|
|
||||||
post = PostCreator.new(
|
post = PostCreator.new(
|
||||||
actor: actor,
|
actor: actor,
|
||||||
attributes: normalised_attributes(attributes, preflight, index)).create!(locale)
|
attributes: normalised_attributes(attributes, preflight, index)).create!
|
||||||
result = {
|
result = {
|
||||||
status: 'created',
|
status: 'created',
|
||||||
post: { id: post.id } }
|
post: { id: post.id } }
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ class PostCreatePlan
|
|||||||
@existing_tags_by_name = nil
|
@existing_tags_by_name = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def build! locale
|
def build!
|
||||||
direct_tag_specs, tag_sections = parse_direct_tag_specs(locale)
|
direct_tag_specs, tag_sections = parse_direct_tag_specs
|
||||||
default_tag_specs = build_default_tag_specs(locale, 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)
|
snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs)
|
||||||
preload_existing_tags_by_name!(locale, snapshot_tag_specs.map { _1[:name] })
|
preload_existing_tags_by_name!(snapshot_tag_specs.map { _1[:name] })
|
||||||
validate_new_tag_specs!(locale, snapshot_tag_specs)
|
validate_new_tag_specs!(snapshot_tag_specs)
|
||||||
post_tag_specs = expand_parent_tag_specs(locale, snapshot_tag_specs)
|
post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
|
||||||
video_ms = normalise_video_ms(snapshot_tag_specs)
|
video_ms = normalise_video_ms(snapshot_tag_specs)
|
||||||
validate_video_sections!(video_ms, tag_sections)
|
validate_video_sections!(video_ms, tag_sections)
|
||||||
parent_post_ids = normalise_parent_post_ids
|
parent_post_ids = normalise_parent_post_ids
|
||||||
@@ -42,16 +42,14 @@ class PostCreatePlan
|
|||||||
|
|
||||||
def tag_names = @attributes[:tags].to_s.split
|
def tag_names = @attributes[:tags].to_s.split
|
||||||
|
|
||||||
def parse_direct_tag_specs locale
|
def parse_direct_tag_specs
|
||||||
tag_sections = { }
|
tag_sections = { }
|
||||||
direct_tag_specs = []
|
direct_tag_specs = []
|
||||||
|
|
||||||
tag_names.each do |raw_name|
|
tag_names.each do |raw_name|
|
||||||
tag_name, category, sections = parse_raw_tag_name(locale, raw_name)
|
tag_name, category, sections = parse_raw_tag_name(raw_name)
|
||||||
existing_tag = existing_tags_by_name(locale)[tag_name]
|
existing_tag = existing_tags_by_name[tag_name]
|
||||||
if existing_tag&.deprecated?
|
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
|
||||||
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name(locale.language_code)]
|
|
||||||
end
|
|
||||||
|
|
||||||
direct_tag_specs << {
|
direct_tag_specs << {
|
||||||
name: tag_name,
|
name: tag_name,
|
||||||
@@ -67,7 +65,7 @@ class PostCreatePlan
|
|||||||
[merge_tag_specs(direct_tag_specs), tag_sections]
|
[merge_tag_specs(direct_tag_specs), tag_sections]
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_raw_tag_name locale, raw_name
|
def parse_raw_tag_name raw_name
|
||||||
name = raw_name.to_s
|
name = raw_name.to_s
|
||||||
prefix, category =
|
prefix, category =
|
||||||
Tag::CATEGORY_PREFIXES.find {
|
Tag::CATEGORY_PREFIXES.find {
|
||||||
@@ -76,7 +74,7 @@ class PostCreatePlan
|
|||||||
name = name.sub(/\A#{ prefix }/i, '')
|
name = name.sub(/\A#{ prefix }/i, '')
|
||||||
|
|
||||||
sections = []
|
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] }"
|
name = "#{ match[1] }#{ match[4] }"
|
||||||
next if match[2].empty? && match[3].empty?
|
next if match[2].empty? && match[3].empty?
|
||||||
|
|
||||||
@@ -89,17 +87,17 @@ class PostCreatePlan
|
|||||||
raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
|
raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
[resolved_tag_name(locale, name), category&.to_sym, sections]
|
[resolved_tag_name(name), category&.to_sym, sections]
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_default_tag_specs locale, direct_tag_specs
|
def build_default_tag_specs direct_tag_specs
|
||||||
default_tag_specs = []
|
default_tag_specs = []
|
||||||
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
||||||
default_tag_specs << {
|
default_tag_specs << {
|
||||||
name: TAGME_TAG_NAME,
|
name: TAGME_TAG_NAME,
|
||||||
category: :meta }
|
category: :meta }
|
||||||
end
|
end
|
||||||
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
|
if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
|
||||||
default_tag_specs << {
|
default_tag_specs << {
|
||||||
name: NO_DEERJIKIST_TAG_NAME,
|
name: NO_DEERJIKIST_TAG_NAME,
|
||||||
category: :meta }
|
category: :meta }
|
||||||
@@ -108,18 +106,17 @@ class PostCreatePlan
|
|||||||
default_tag_specs
|
default_tag_specs
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_new_tag_specs! locale, specs
|
def validate_new_tag_specs! specs
|
||||||
Array(specs).each do |spec|
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_new_tag_spec! locale, spec
|
def validate_new_tag_spec! spec
|
||||||
tag_name = TagName.new(name: spec[:name], language_code: locale.language_code,
|
tag_name = TagName.new(name: spec[:name])
|
||||||
script_code: locale.script_code, primary_flg: true)
|
tag = Tag.new(category: spec[:category], tag_name:)
|
||||||
tag = Tag.new(category: spec[:category])
|
|
||||||
return if tag_name.valid? && tag.valid?
|
return if tag_name.valid? && tag.valid?
|
||||||
|
|
||||||
post = Post.new
|
post = Post.new
|
||||||
@@ -134,15 +131,13 @@ class PostCreatePlan
|
|||||||
raise ActiveRecord::RecordInvalid, post
|
raise ActiveRecord::RecordInvalid, post
|
||||||
end
|
end
|
||||||
|
|
||||||
def expand_parent_tag_specs locale, snapshot_tag_specs
|
def expand_parent_tag_specs snapshot_tag_specs
|
||||||
existing_snapshot_tags = snapshot_tag_specs.filter_map do
|
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
|
||||||
existing_tags_by_name(locale)[_1[:name]]
|
|
||||||
end
|
|
||||||
expanded_parent_specs =
|
expanded_parent_specs =
|
||||||
Tag.expand_parent_tags(existing_snapshot_tags)
|
Tag.expand_parent_tags(existing_snapshot_tags)
|
||||||
.reject(&:deprecated?)
|
.reject(&:deprecated?)
|
||||||
.map { |tag|
|
.map { |tag|
|
||||||
{ name: tag.name(locale.language_code),
|
{ name: tag.name,
|
||||||
category: tag.category.to_sym }
|
category: tag.category.to_sym }
|
||||||
}
|
}
|
||||||
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
||||||
@@ -161,37 +156,33 @@ class PostCreatePlan
|
|||||||
}.values.sort_by { _1[:name] }
|
}.values.sort_by { _1[:name] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def existing_tags_by_name locale
|
def existing_tags_by_name
|
||||||
@existing_tags_by_name ||= begin
|
@existing_tags_by_name ||= begin
|
||||||
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
|
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
|
||||||
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
|
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
|
||||||
.includes(:tag)
|
|
||||||
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
|
|
||||||
end
|
end
|
||||||
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
|
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?
|
return if missing_names.empty?
|
||||||
|
|
||||||
existing_tags_by_name(locale).merge!(
|
existing_tags_by_name.merge!(
|
||||||
TagName.where(language_code: locale.language_code,
|
Tag.joins(:tag_name)
|
||||||
primary_flg: true,
|
.where(tag_names: { name: missing_names })
|
||||||
name: missing_names)
|
.index_by(&:name))
|
||||||
.includes(:tag)
|
|
||||||
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def canonical_tag_name_without_sections locale, raw_name
|
def canonical_tag_name_without_sections raw_name
|
||||||
name, = parse_raw_tag_name(locale, raw_name)
|
name, = parse_raw_tag_name(raw_name)
|
||||||
name
|
name
|
||||||
end
|
end
|
||||||
|
|
||||||
def deerjikist_tag_spec? locale, spec
|
def deerjikist_tag_spec? spec
|
||||||
return true if spec[:category] == :deerjikist
|
return true if spec[:category] == :deerjikist
|
||||||
|
|
||||||
existing_tags_by_name(locale)[spec[:name]]&.deerjikist?
|
existing_tags_by_name[spec[:name]]&.deerjikist?
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalise_parent_post_ids
|
def normalise_parent_post_ids
|
||||||
@@ -269,12 +260,10 @@ class PostCreatePlan
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolved_tag_name locale, name
|
def resolved_tag_name name
|
||||||
tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
tag_name = TagName.includes(:canonical).find_by(name:)
|
||||||
return name if !(tag_name) || tag_name.primary?
|
return name if tag_name.nil?
|
||||||
|
|
||||||
TagName.find_by(language_code: locale.language_code,
|
(tag_name.canonical || tag_name).name
|
||||||
tag_id: tag_name.tag_id,
|
|
||||||
primary_flg: true)&.name || name
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ class PostCreatePreflight
|
|||||||
@host = host
|
@host = host
|
||||||
end
|
end
|
||||||
|
|
||||||
def run locale
|
def run
|
||||||
preview = PostImportPreviewer.new.preview_rows(
|
preview = PostImportPreviewer.new.preview_rows(
|
||||||
rows: [preview_row],
|
rows: [preview_row],
|
||||||
locale:,
|
|
||||||
fetch_metadata: false).first
|
fetch_metadata: false).first
|
||||||
if preview[:existing_post_id].present?
|
if preview[:existing_post_id].present?
|
||||||
return {
|
return {
|
||||||
@@ -54,7 +53,7 @@ class PostCreatePreflight
|
|||||||
original_created_from: preview[:attributes]['original_created_from'],
|
original_created_from: preview[:attributes]['original_created_from'],
|
||||||
original_created_before: preview[:attributes]['original_created_before'],
|
original_created_before: preview[:attributes]['original_created_before'],
|
||||||
duration: preview[:attributes]['duration'],
|
duration: preview[:attributes]['duration'],
|
||||||
video_ms: preview[:attributes]['video_ms'] }).build!(locale)
|
video_ms: preview[:attributes]['video_ms'] }).build!
|
||||||
|
|
||||||
{
|
{
|
||||||
url: plan[:url],
|
url: plan[:url],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class PostCreator
|
|||||||
@field_warnings = { }
|
@field_warnings = { }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create! locale
|
def create!
|
||||||
thumbnail_attachment = prepare_thumbnail_attachment
|
thumbnail_attachment = prepare_thumbnail_attachment
|
||||||
post = Post.new(title: @attributes[:title].presence,
|
post = Post.new(title: @attributes[:title].presence,
|
||||||
url: @attributes[:url],
|
url: @attributes[:url],
|
||||||
@@ -21,17 +21,14 @@ class PostCreator
|
|||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
post.save!
|
post.save!
|
||||||
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
|
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
|
||||||
snapshot_tags = planned_snapshot_tags(locale)
|
snapshot_tags = planned_snapshot_tags
|
||||||
post_tags = planned_post_tags(locale)
|
post_tags = planned_post_tags
|
||||||
sections = planned_sections(locale)
|
sections = planned_sections
|
||||||
TagVersioning.record_tag_snapshots!(
|
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
|
||||||
snapshot_tags,
|
post.video_ms = planned_video_ms
|
||||||
created_by_user: @actor,
|
|
||||||
language_code: locale.language_code)
|
|
||||||
post.video_ms = planned_video_ms(locale)
|
|
||||||
post.save!
|
post.save!
|
||||||
sync_post_tags!(post, post_tags, sections)
|
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)
|
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
|
||||||
end
|
end
|
||||||
post
|
post
|
||||||
@@ -48,73 +45,69 @@ class PostCreator
|
|||||||
thumbnail_base: @attributes[:thumbnail_base].presence)
|
thumbnail_base: @attributes[:thumbnail_base].presence)
|
||||||
end
|
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
|
def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]
|
||||||
planned_create_attributes(locale)[:normalised_parent_post_ids]
|
|
||||||
|
def planned_video_ms
|
||||||
|
planned_create_attributes[:video_ms]
|
||||||
end
|
end
|
||||||
|
|
||||||
def planned_video_ms locale
|
def planned_create_attributes
|
||||||
planned_create_attributes(locale)[:video_ms]
|
|
||||||
end
|
|
||||||
|
|
||||||
def planned_create_attributes locale
|
|
||||||
@planned_create_attributes ||= begin
|
@planned_create_attributes ||= begin
|
||||||
if @attributes.key?(:snapshot_tag_specs)
|
if @attributes.key?(:snapshot_tag_specs)
|
||||||
snapshot_tags = materialise_tags(locale, @attributes[:snapshot_tag_specs] || [])
|
snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
|
||||||
post_tags = materialise_tags(locale, @attributes[:post_tag_specs] || [])
|
post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
|
||||||
{
|
{
|
||||||
snapshot_tags: snapshot_tags,
|
snapshot_tags: snapshot_tags,
|
||||||
post_tags: post_tags,
|
post_tags: post_tags,
|
||||||
tag_sections: materialise_sections(
|
tag_sections: materialise_sections(
|
||||||
@attributes[:tag_sections] || { },
|
@attributes[:tag_sections] || { },
|
||||||
snapshot_tags,
|
snapshot_tags,
|
||||||
post_tags,
|
post_tags),
|
||||||
locale),
|
|
||||||
normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
|
normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
|
||||||
video_ms: @attributes[:video_ms] }
|
video_ms: @attributes[:video_ms] }
|
||||||
else
|
else
|
||||||
build_materialised_plan(locale)
|
build_materialised_plan
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_materialised_plan locale
|
def build_materialised_plan
|
||||||
plan = PostCreatePlan.new(attributes: @attributes).build!(locale)
|
plan = PostCreatePlan.new(attributes: @attributes).build!
|
||||||
snapshot_tags = materialise_tags(locale, plan[:snapshot_tag_specs] || [])
|
snapshot_tags = materialise_tags(plan[:snapshot_tag_specs] || [])
|
||||||
post_tags = materialise_tags(locale, plan[:post_tag_specs] || [])
|
post_tags = materialise_tags(plan[:post_tag_specs] || [])
|
||||||
{
|
{
|
||||||
snapshot_tags: snapshot_tags,
|
snapshot_tags: snapshot_tags,
|
||||||
post_tags: post_tags,
|
post_tags: post_tags,
|
||||||
tag_sections: materialise_sections(
|
tag_sections: materialise_sections(
|
||||||
plan[:tag_sections] || { },
|
plan[:tag_sections] || { },
|
||||||
snapshot_tags,
|
snapshot_tags,
|
||||||
post_tags,
|
post_tags),
|
||||||
locale),
|
|
||||||
normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
|
normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
|
||||||
video_ms: plan[:video_ms] }
|
video_ms: plan[:video_ms] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def materialise_tags locale, specs
|
def materialise_tags specs
|
||||||
Array(specs).each_with_object({ }) do |spec, tags|
|
Array(specs).each_with_object({ }) do |spec, tags|
|
||||||
name = spec[:name] || spec['name']
|
name = spec[:name] || spec['name']
|
||||||
category = spec[:category] || spec['category']
|
category = spec[:category] || spec['category']
|
||||||
next if name.blank? || category.blank?
|
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
|
tag.update!(category:) if tag.category.to_sym != category.to_sym
|
||||||
tags[name] ||= tag
|
tags[name] ||= tag
|
||||||
end.values
|
end.values
|
||||||
end
|
end
|
||||||
|
|
||||||
def materialise_sections sections_by_name, snapshot_tags, post_tags, locale = Locale.nipponese
|
def materialise_sections sections_by_name, snapshot_tags, post_tags
|
||||||
tags_by_name = post_tags.index_by { _1.name(locale.language_code) }
|
tags_by_name = post_tags.index_by(&:name)
|
||||||
snapshot_tags.each do |tag|
|
snapshot_tags.each do |tag|
|
||||||
tags_by_name[tag.name(locale.language_code)] ||= tag
|
tags_by_name[tag.name] ||= tag
|
||||||
end
|
end
|
||||||
|
|
||||||
sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
|
sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PostImportPreviewer
|
|||||||
THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze
|
THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze
|
||||||
METADATA_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) }
|
prepared_rows = rows.map { prepare_row(_1) }
|
||||||
url_counts = prepared_rows.filter_map { _1[:normal_url] }.tally
|
url_counts = prepared_rows.filter_map { _1[:normal_url] }.tally
|
||||||
existing_posts =
|
existing_posts =
|
||||||
@@ -30,14 +30,13 @@ class PostImportPreviewer
|
|||||||
url_counts)
|
url_counts)
|
||||||
known_tags =
|
known_tags =
|
||||||
preload_known_tags(
|
preload_known_tags(
|
||||||
locale,
|
|
||||||
prepared_rows,
|
prepared_rows,
|
||||||
fetch_metadata,
|
fetch_metadata,
|
||||||
metadata_cache,
|
metadata_cache,
|
||||||
existing_posts,
|
existing_posts,
|
||||||
url_counts)
|
url_counts)
|
||||||
prepared_rows.map { |row|
|
prepared_rows.map { |row|
|
||||||
preview_row(locale, row,
|
preview_row(row,
|
||||||
fetch_metadata:,
|
fetch_metadata:,
|
||||||
metadata_cache:,
|
metadata_cache:,
|
||||||
existing_posts:,
|
existing_posts:,
|
||||||
@@ -60,7 +59,7 @@ class PostImportPreviewer
|
|||||||
source.merge(url_text: url, normal_url:, url_error: validate_url_safety(normal_url))
|
source.merge(url_text: url, normal_url:, url_error: validate_url_safety(normal_url))
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview_row locale, row,
|
def preview_row row,
|
||||||
fetch_metadata:,
|
fetch_metadata:,
|
||||||
metadata_cache:,
|
metadata_cache:,
|
||||||
existing_posts:,
|
existing_posts:,
|
||||||
@@ -125,8 +124,7 @@ class PostImportPreviewer
|
|||||||
|
|
||||||
attributes['url'] = url
|
attributes['url'] = url
|
||||||
validate_basic_data(attributes, validation_errors)
|
validate_basic_data(attributes, validation_errors)
|
||||||
validate_preview_tags(locale,
|
validate_preview_tags(merged_tags(tag_sources, provenance['tags']),
|
||||||
merged_tags(tag_sources, provenance['tags']),
|
|
||||||
validation_errors,
|
validation_errors,
|
||||||
known_tags)
|
known_tags)
|
||||||
validate_parents(attributes['parent_post_ids'], validation_errors, existing_parent_ids)
|
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: { } }
|
{ data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } }
|
||||||
end
|
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|
|
names = prepared_rows.flat_map { |row|
|
||||||
attributes = initial_attributes(row)
|
attributes = initial_attributes(row)
|
||||||
provenance = initial_provenance(row)
|
provenance = initial_provenance(row)
|
||||||
@@ -360,13 +358,15 @@ class PostImportPreviewer
|
|||||||
metadata = metadata_for(row[:normal_url], metadata_cache)
|
metadata = metadata_for(row[:normal_url], metadata_cache)
|
||||||
apply_metadata!(attributes, provenance, tag_sources, metadata[:data])
|
apply_metadata!(attributes, provenance, tag_sources, metadata[:data])
|
||||||
end
|
end
|
||||||
preview_tag_names(locale, merged_tags(tag_sources, provenance['tags']))
|
preview_tag_names(merged_tags(tag_sources, provenance['tags']))
|
||||||
}.compact.uniq
|
}.compact.uniq
|
||||||
return { } if names.empty?
|
return { } if names.empty?
|
||||||
|
|
||||||
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
|
Tag.joins(:tag_name)
|
||||||
.includes(:tag)
|
.where(tag_names: { name: names })
|
||||||
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
|
.includes(:tag_name)
|
||||||
|
.to_a
|
||||||
|
.index_by(&:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def preload_parent_ids prepared_rows
|
def preload_parent_ids prepared_rows
|
||||||
@@ -437,21 +437,19 @@ class PostImportPreviewer
|
|||||||
sources['automatic'].to_s
|
sources['automatic'].to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview_tag_names locale, raw
|
def preview_tag_names raw
|
||||||
names = raw.to_s.split
|
names = raw.to_s.split
|
||||||
return [] if names.empty?
|
return [] if names.empty?
|
||||||
if names.any? { _1.downcase.start_with?('nico:') }
|
if names.any? { _1.downcase.start_with?('nico:') }
|
||||||
return []
|
return []
|
||||||
end
|
end
|
||||||
|
|
||||||
names.map { |name|
|
names.map { |name| TagName.canonicalise(name.sub(/\[.*\]\z/, '')).first }
|
||||||
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
|
|
||||||
}
|
|
||||||
rescue Tag::SectionLiteralParseError
|
rescue Tag::SectionLiteralParseError
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_preview_tags locale, raw, errors, known_tags
|
def validate_preview_tags raw, errors, known_tags
|
||||||
names = raw.to_s.split
|
names = raw.to_s.split
|
||||||
return if names.empty?
|
return if names.empty?
|
||||||
if names.any? { _1.downcase.start_with?('nico:') }
|
if names.any? { _1.downcase.start_with?('nico:') }
|
||||||
@@ -459,11 +457,9 @@ class PostImportPreviewer
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
parsed = names.map { |name|
|
parsed = names.map { |name| TagName.canonicalise(name.sub(/\[.*\]\z/, '')).first }
|
||||||
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
|
|
||||||
}
|
|
||||||
existing = parsed.filter_map { known_tags[_1] }
|
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?
|
errors[:tags] = ["廃止済みタグがあります: #{ deprecated.join(' ') }"] if deprecated.present?
|
||||||
rescue Tag::SectionLiteralParseError
|
rescue Tag::SectionLiteralParseError
|
||||||
errors[:tags] = ['タグ区間の記法が不正です.']
|
errors[:tags] = ['タグ区間の記法が不正です.']
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ class PostMetadataFetcher
|
|||||||
|
|
||||||
existing_tags =
|
existing_tags =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_names)
|
.joins(:tag_name)
|
||||||
.where(tag_names: { language_code: 'ja', primary_flg: true, name: names })
|
.where(tag_names: { name: names })
|
||||||
.index_by { _1.name('ja') }
|
.index_by(&:name)
|
||||||
|
|
||||||
names.map { |name|
|
names.map { |name|
|
||||||
tag = existing_tags[name]
|
tag = existing_tags[name]
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
class TagDescriptionOperation
|
|
||||||
LANGUAGE_CODE = 'ja'
|
|
||||||
|
|
||||||
def self.create!(tag:, body:, created_by_user:, message: nil)
|
|
||||||
ApplicationRecord.transaction do
|
|
||||||
tag.lock!
|
|
||||||
raise ActiveRecord::RecordNotUnique if TagDescription.exists?(
|
|
||||||
tag_id: tag.id, language_code: LANGUAGE_CODE)
|
|
||||||
|
|
||||||
page = Wiki::CompatibilityWriter.create!(
|
|
||||||
tag_name: tag.tag_name(LANGUAGE_CODE), body:, created_by_user:, message:)
|
|
||||||
revision = page.wiki_revisions.order(id: :desc).first
|
|
||||||
record_version!(tag:, revision:, body:, user: created_by_user,
|
|
||||||
description: nil)
|
|
||||||
page
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.update!(tag:, body:, updated_by_user:, message: nil,
|
|
||||||
base_revision_id: nil)
|
|
||||||
ApplicationRecord.transaction do
|
|
||||||
tag.lock!
|
|
||||||
description = TagDescription.lock.find_by!(
|
|
||||||
tag_id: tag.id, language_code: LANGUAGE_CODE)
|
|
||||||
page = WikiPage.lock.find(description.id)
|
|
||||||
revision = Wiki::CompatibilityWriter.update!(
|
|
||||||
page:, body:, updated_by_user:, message:,
|
|
||||||
base_revision_id:)
|
|
||||||
next unless revision
|
|
||||||
|
|
||||||
record_version!(tag:, revision:, body: revision.body,
|
|
||||||
user: updated_by_user, description:)
|
|
||||||
revision
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.record_version! tag:, revision:, body:, user:, description:
|
|
||||||
version_no = description ? description.version_no + 1 : 1
|
|
||||||
TagDescriptionVersion.create!(
|
|
||||||
id: revision.id, tag_id: tag.id, language_code: LANGUAGE_CODE,
|
|
||||||
version_no:, event_type: description ? 'update' : 'create', body:,
|
|
||||||
created_by_user_id: user.id, created_at: revision.created_at)
|
|
||||||
if description
|
|
||||||
description.update!(body:, version_no:, updated_by_user_id: user.id)
|
|
||||||
else
|
|
||||||
TagDescription.create!(
|
|
||||||
id: revision.wiki_page_id, tag_id: tag.id,
|
|
||||||
language_code: LANGUAGE_CODE, version_no:, body:,
|
|
||||||
created_by_user_id: user.id, updated_by_user_id: user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private_class_method :record_version!
|
|
||||||
end
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
require 'kramdown'
|
|
||||||
|
|
||||||
class TagGroupMarkdownParser
|
|
||||||
PREFIX = TagGroupRepr::PREFIX
|
|
||||||
|
|
||||||
class InvalidDocument < StandardError
|
|
||||||
;
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.parse(body:, group: nil)
|
|
||||||
new(body:, group:).parse
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(body:, group:)
|
|
||||||
@body = body.to_s.gsub(/\r\n?/, "\n")
|
|
||||||
@group = group
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse
|
|
||||||
parse_body = @body.gsub(/([^\n])\n([\x23]{1,6}[ \t]+\S)/, "\\1\n\n\\2")
|
|
||||||
root = Kramdown::Document.new(parse_body, input: 'GFM', auto_ids: false).root
|
|
||||||
placements = []
|
|
||||||
description_parts = []
|
|
||||||
headings = []
|
|
||||||
|
|
||||||
root.children.each do |node|
|
|
||||||
case node.type
|
|
||||||
when :blank
|
|
||||||
next
|
|
||||||
when :header
|
|
||||||
level = node.options.fetch(:level)
|
|
||||||
name = inline_text(node).strip
|
|
||||||
invalid!('Section 名は必須です.') if name.blank?
|
|
||||||
|
|
||||||
headings.pop while headings.any? && headings.last.fetch(:level) >= level
|
|
||||||
parent = headings.last&.fetch(:node)
|
|
||||||
section = { 'section_name' => name, 'description' => nil,
|
|
||||||
'children' => [] }
|
|
||||||
(parent ? parent.fetch('children') : placements) << section
|
|
||||||
headings << { level:, node: section }
|
|
||||||
when :p
|
|
||||||
text = inline_markdown(node).strip
|
|
||||||
next if text.empty?
|
|
||||||
|
|
||||||
if headings.any?
|
|
||||||
section = headings.last.fetch(:node)
|
|
||||||
section['description'] = [section['description'], text].compact.join("\n\n")
|
|
||||||
else
|
|
||||||
description_parts << text
|
|
||||||
end
|
|
||||||
when :ul
|
|
||||||
invalid!('未対応のリスト項目があります.') if node.attr.any?
|
|
||||||
target = headings.any? ? headings.last.fetch(:node).fetch('children') : placements
|
|
||||||
target.concat(node.children.map { parse_list_item(_1) })
|
|
||||||
else
|
|
||||||
invalid!("未対応の Markdown 構文 #{ node.type.inspect } です.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
description = description_parts.presence&.join("\n\n")
|
|
||||||
{ description:, placements:, members: build_members(placements),
|
|
||||||
compatibility_body: @body.sub(/\n+\z/, '') }
|
|
||||||
rescue Kramdown::Error => error
|
|
||||||
invalid!("Markdown を解析できません:#{ error.message }")
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parse_list_item(item)
|
|
||||||
invalid!('リスト項目ではありません.') unless item.type == :li
|
|
||||||
invalid!('未対応のリスト項目があります.') if item.attr.any?
|
|
||||||
|
|
||||||
nested, own = item.children.partition { |child| child.type == :ul }
|
|
||||||
invalid!('順序付きリストは使用できません.') if own.any? { _1.type == :ol }
|
|
||||||
own.reject! { _1.type == :blank }
|
|
||||||
invalid!('未対応のリスト項目があります.') unless own.all? do |child|
|
|
||||||
[:p, :text].include?(child.type)
|
|
||||||
end
|
|
||||||
invalid!('リスト項目は一つの段落で指定してください.') if own.size != 1
|
|
||||||
|
|
||||||
label = own.map { inline_text(_1) }.join(' ').strip
|
|
||||||
invalid!('空のリスト項目は使用できません.') if label.blank?
|
|
||||||
|
|
||||||
node = if label.start_with?(PREFIX)
|
|
||||||
group_name = label.delete_prefix(PREFIX).strip
|
|
||||||
child_group = TagGroup.find_by(name: group_name)
|
|
||||||
invalid!("Tag Group #{ group_name.inspect } を解決できません.") unless child_group
|
|
||||||
invalid!('自分自身を含めることはできません.') if child_group.id == @group&.id
|
|
||||||
{ 'tag_group_id' => child_group.id }
|
|
||||||
elsif label.end_with?(':', ':')
|
|
||||||
section_name = label.sub(/[::]\z/, '').strip
|
|
||||||
invalid!('Section 名は必須です.') if section_name.blank?
|
|
||||||
{ 'section_name' => section_name, 'description' => nil }
|
|
||||||
else
|
|
||||||
{ 'tag_id' => resolve_tag(label) }
|
|
||||||
end
|
|
||||||
|
|
||||||
node['children'] = nested.flat_map do |list|
|
|
||||||
invalid!('未対応の入れ子リストです.') unless list.type == :ul
|
|
||||||
list.children.map { parse_list_item(_1) }
|
|
||||||
end
|
|
||||||
node
|
|
||||||
end
|
|
||||||
|
|
||||||
def resolve_tag(name)
|
|
||||||
names = TagName.where(language_code: 'ja', name:).to_a
|
|
||||||
tag_ids = names.filter_map(&:tag_id).uniq
|
|
||||||
invalid!("Tag #{ name.inspect } を解決できません.") if tag_ids.empty?
|
|
||||||
invalid!("Tag 名 #{ name.inspect } が曖昧です.") if tag_ids.many?
|
|
||||||
|
|
||||||
tag_ids.first
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_members(nodes)
|
|
||||||
references = []
|
|
||||||
visit = lambda do |items|
|
|
||||||
items.each do |node|
|
|
||||||
references << node.slice('tag_id', 'tag_group_id') if
|
|
||||||
node.key?('tag_id') || node.key?('tag_group_id')
|
|
||||||
visit.call(node.fetch('children'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
visit.call(nodes)
|
|
||||||
references.uniq
|
|
||||||
end
|
|
||||||
|
|
||||||
def inline_text(node)
|
|
||||||
if node.attr.any?
|
|
||||||
invalid!("未対応のインライン構文 #{ node.type.inspect } です.")
|
|
||||||
end
|
|
||||||
|
|
||||||
case node.type
|
|
||||||
when :text
|
|
||||||
node.value.to_s
|
|
||||||
when :p, :header
|
|
||||||
node.children.map { inline_text(_1) }.join
|
|
||||||
else
|
|
||||||
invalid!("未対応のインライン構文 #{ node.type.inspect } です.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def inline_markdown(node)
|
|
||||||
case node.type
|
|
||||||
when :text
|
|
||||||
node.value.to_s
|
|
||||||
when :codespan
|
|
||||||
text = node.value.to_s
|
|
||||||
longest = text.scan(/`+/).map(&:length).max || 0
|
|
||||||
delimiter = '`' * (longest + 1)
|
|
||||||
padded = text.start_with?('`', ' ') || text.end_with?('`', ' ')
|
|
||||||
content = padded ? " #{ text } " : text
|
|
||||||
"#{ delimiter }#{ content }#{ delimiter }"
|
|
||||||
when :softbreak
|
|
||||||
"\n"
|
|
||||||
when :br
|
|
||||||
" \n"
|
|
||||||
when :p
|
|
||||||
node.children.map { inline_markdown(_1) }.join
|
|
||||||
when :a
|
|
||||||
href = node.attr.fetch('href')
|
|
||||||
label = node.children.map { inline_markdown(_1) }.join
|
|
||||||
title = node.attr['title']&.gsub('"', '\\"')
|
|
||||||
suffix = title ? " \"#{ title }\"" : ''
|
|
||||||
"[#{ label }](#{ href }#{ suffix })"
|
|
||||||
when :em
|
|
||||||
"*#{ node.children.map { inline_markdown(_1) }.join }*"
|
|
||||||
when :strong
|
|
||||||
"**#{ node.children.map { inline_markdown(_1) }.join }**"
|
|
||||||
else
|
|
||||||
invalid!("未対応の説明文構文 #{ node.type.inspect } です.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def invalid! message
|
|
||||||
raise InvalidDocument, message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
class TagGroupMarkdownRenderer
|
|
||||||
class UnsupportedStructure < StandardError
|
|
||||||
;
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.render group
|
|
||||||
new(group).render
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize group
|
|
||||||
@group = group
|
|
||||||
end
|
|
||||||
|
|
||||||
def render
|
|
||||||
placements = @group.fetch('placements')
|
|
||||||
validate_membership!(placements)
|
|
||||||
blocks = []
|
|
||||||
blocks << clean_text(@group['description']) if @group['description'].present?
|
|
||||||
|
|
||||||
sections = placements.select { _1.key?('section_name') }
|
|
||||||
if sections.any? && sections.size != placements.size
|
|
||||||
raise UnsupportedStructure,
|
|
||||||
'最上位の見出し Section と Placement の混在は Markdown にできません.'
|
|
||||||
end
|
|
||||||
|
|
||||||
if placements.all? { _1.key?('section_name') }
|
|
||||||
blocks.concat(placements.map { render_heading(_1, 1) })
|
|
||||||
else
|
|
||||||
blocks << render_list(placements, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
blocks.compact.reject(&:empty?).join("\n").sub(/\n+\z/, '')
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def validate_membership! placements
|
|
||||||
placed = placement_members(placements).uniq.sort
|
|
||||||
members = @group.fetch('members').map do |member|
|
|
||||||
if member['tag']
|
|
||||||
['tag', member.fetch('tag').fetch('id')]
|
|
||||||
else
|
|
||||||
['tag_group', member.fetch('tag_group').fetch('id')]
|
|
||||||
end
|
|
||||||
end.uniq.sort
|
|
||||||
missing = members - placed
|
|
||||||
raise UnsupportedStructure, '掲載位置のない membership は Markdown にできません.' if missing.any?
|
|
||||||
end
|
|
||||||
|
|
||||||
def placement_members nodes
|
|
||||||
nodes.flat_map do |node|
|
|
||||||
own = if node['tag']
|
|
||||||
[['tag', node.fetch('tag').fetch('id')]]
|
|
||||||
elsif node['tag_group']
|
|
||||||
[['tag_group', node.fetch('tag_group').fetch('id')]]
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
own + placement_members(node.fetch('children'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_heading section, level
|
|
||||||
header = "#{ '#' * level } #{ clean_text(section.fetch('section_name')) }"
|
|
||||||
blocks = [header]
|
|
||||||
blocks << clean_text(section['description']) if section['description'].present?
|
|
||||||
children = section.fetch('children')
|
|
||||||
section_indexes = children.each_index.select { |index| children[index].key?('section_name') }
|
|
||||||
if section_indexes.any?
|
|
||||||
first_section = section_indexes.first
|
|
||||||
if children[first_section..].any? { !_1.key?('section_name') }
|
|
||||||
raise UnsupportedStructure,
|
|
||||||
'見出し Section と兄弟 Placement の交互配置は Markdown にできません.'
|
|
||||||
end
|
|
||||||
list_nodes = children[0...first_section]
|
|
||||||
blocks << render_list(list_nodes, 0) if list_nodes.any?
|
|
||||||
blocks.concat(children[first_section..].map { render_heading(_1, level + 1) })
|
|
||||||
elsif children.any?
|
|
||||||
blocks << render_list(children, 0)
|
|
||||||
end
|
|
||||||
blocks.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_list nodes, depth
|
|
||||||
nodes.map do |node|
|
|
||||||
indent = ' ' * depth
|
|
||||||
if node.key?('section_name')
|
|
||||||
raise UnsupportedStructure,
|
|
||||||
'リスト内 Section の説明文は Markdown にできません.' if node['description'].present?
|
|
||||||
|
|
||||||
children = node.fetch('children')
|
|
||||||
head = "#{ indent }- #{ clean_text(node.fetch('section_name')) }:"
|
|
||||||
tail = render_list(children, depth + 1) if children.any?
|
|
||||||
[head, tail].compact.join("\n")
|
|
||||||
else
|
|
||||||
label = if node['tag']
|
|
||||||
node.fetch('tag').fetch('name')
|
|
||||||
else
|
|
||||||
"#{ TagGroupRepr::PREFIX }#{ node.fetch('tag_group').fetch('name') }"
|
|
||||||
end
|
|
||||||
raise UnsupportedStructure, '名称のない Placement は Markdown にできません.' if label.blank?
|
|
||||||
|
|
||||||
head = "#{ indent }- #{ clean_text(label) }"
|
|
||||||
children = node.fetch('children')
|
|
||||||
tail = render_list(children, depth + 1) if children.any?
|
|
||||||
[head, tail].compact.join("\n")
|
|
||||||
end
|
|
||||||
end.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def clean_text value
|
|
||||||
value.to_s.gsub(/\r\n?/, "\n").lines.map(&:rstrip).join("\n").strip
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
class TagGroupOperation
|
|
||||||
MAX_PLACEMENT_DEPTH = 5
|
|
||||||
PREFIX = TagGroupRepr::PREFIX
|
|
||||||
|
|
||||||
def self.create!(name:, description:, members:, placements:, actor:,
|
|
||||||
require_compatibility: false, compatibility_body: nil)
|
|
||||||
group, sync_issue = ApplicationRecord.transaction do
|
|
||||||
group = TagGroup.new(version_no: 1, created_by_user: actor,
|
|
||||||
updated_by_user: actor)
|
|
||||||
attributes = prepare!(group:, name:, description:, members:, placements:)
|
|
||||||
group.assign_attributes(name: attributes.fetch(:name),
|
|
||||||
description: attributes.fetch(:description))
|
|
||||||
group.save!
|
|
||||||
persist_structure!(group:, attributes:, actor:)
|
|
||||||
persist_version!(group:, attributes:, actor:, event_type: 'create')
|
|
||||||
issue = sync_compatibility!(group:, actor:, old_name: nil,
|
|
||||||
require_compatibility:, compatibility_body:)
|
|
||||||
[group, issue]
|
|
||||||
end
|
|
||||||
report_sync_issue(group, actor, sync_issue)
|
|
||||||
group
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.update!(group:, name:, description:, members:, placements:, actor:,
|
|
||||||
require_compatibility: false, compatibility_body: nil)
|
|
||||||
group, sync_issue = ApplicationRecord.transaction do
|
|
||||||
group.lock!
|
|
||||||
old_name = group.name
|
|
||||||
attributes = prepare!(group:, name:, description:, members:, placements:)
|
|
||||||
group.update!(name: attributes.fetch(:name),
|
|
||||||
description: attributes.fetch(:description),
|
|
||||||
version_no: group.version_no + 1,
|
|
||||||
updated_by_user: actor)
|
|
||||||
persist_structure!(group:, attributes:, actor:)
|
|
||||||
persist_version!(group:, attributes:, actor:, event_type: 'update')
|
|
||||||
issue = sync_compatibility!(group:, actor:, old_name:,
|
|
||||||
require_compatibility:, compatibility_body:)
|
|
||||||
[group, issue]
|
|
||||||
end
|
|
||||||
report_sync_issue(group, actor, sync_issue)
|
|
||||||
group
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.prepare!(group:, name:, description:, members:, placements:)
|
|
||||||
record = group
|
|
||||||
normalized_name = name.to_s.strip
|
|
||||||
invalid!(record, :name, 'グループ名は必須です.') if normalized_name.blank?
|
|
||||||
if normalized_name.length > 255
|
|
||||||
invalid!(record, :name, 'グループ名は 255 文字以内で入力してください.')
|
|
||||||
end
|
|
||||||
invalid!(record, :description, '説明文は文字列で入力してください.') unless
|
|
||||||
description.nil? || description.is_a?(String)
|
|
||||||
|
|
||||||
record.name = normalized_name
|
|
||||||
record.description = description
|
|
||||||
unless record.valid?
|
|
||||||
raise ActiveRecord::RecordInvalid, record
|
|
||||||
end
|
|
||||||
|
|
||||||
member_rows = normalize_members(record, members)
|
|
||||||
placement_tree = normalize_placements(record, placements)
|
|
||||||
validate_references!(record, member_rows, placement_tree)
|
|
||||||
validate_containment!(record, member_rows)
|
|
||||||
{ name: normalized_name, description:, members: member_rows,
|
|
||||||
placements: placement_tree }
|
|
||||||
end
|
|
||||||
private_class_method :prepare!
|
|
||||||
|
|
||||||
def self.normalize_members(record, members)
|
|
||||||
invalid!(record, :members, 'members は配列で指定してください.') unless members.is_a?(Array)
|
|
||||||
|
|
||||||
result = members.map do |member|
|
|
||||||
member = stringify_keys(member)
|
|
||||||
unless member.is_a?(Hash) && member.keys.one? &&
|
|
||||||
(member.key?('tag_id') || member.key?('tag_group_id'))
|
|
||||||
invalid!(record, :members, '各 member は tag_id または tag_group_id を一つ指定してください.')
|
|
||||||
end
|
|
||||||
key = member.keys.first
|
|
||||||
id = integer_id(record, member.fetch(key), :members)
|
|
||||||
{ key => id }
|
|
||||||
end
|
|
||||||
invalid!(record, :members, 'membership に重複があります.') unless result.uniq == result
|
|
||||||
result
|
|
||||||
end
|
|
||||||
private_class_method :normalize_members
|
|
||||||
|
|
||||||
def self.normalize_placements(record, placements)
|
|
||||||
invalid!(record, :placements, 'placements は配列で指定してください.') unless
|
|
||||||
placements.is_a?(Array)
|
|
||||||
|
|
||||||
normalize_placement_nodes(record, placements, 1)
|
|
||||||
end
|
|
||||||
private_class_method :normalize_placements
|
|
||||||
|
|
||||||
def self.normalize_placement_nodes(record, nodes, depth)
|
|
||||||
if depth > MAX_PLACEMENT_DEPTH && nodes.any?
|
|
||||||
invalid!(record, :placements, '配置の深さは 5 階層までです.')
|
|
||||||
end
|
|
||||||
|
|
||||||
nodes.map do |raw_node|
|
|
||||||
node = stringify_keys(raw_node)
|
|
||||||
unless node.is_a?(Hash) && node.key?('children')
|
|
||||||
invalid!(record, :placements, '各 Placement に children 配列が必要です.')
|
|
||||||
end
|
|
||||||
children = node.delete('children')
|
|
||||||
unless children.is_a?(Array)
|
|
||||||
invalid!(record, :placements, 'children は配列で指定してください.')
|
|
||||||
end
|
|
||||||
|
|
||||||
kinds = ['tag_id', 'tag_group_id', 'section_name'].select { node.key?(_1) }
|
|
||||||
invalid!(record, :placements, 'Placement の種類を一つ指定してください.') unless
|
|
||||||
kinds.one?
|
|
||||||
kind = kinds.first
|
|
||||||
allowed = [kind, 'description']
|
|
||||||
invalid!(record, :placements, '不明な Placement 項目があります.') unless
|
|
||||||
(node.keys - allowed).empty?
|
|
||||||
|
|
||||||
case kind
|
|
||||||
when 'tag_id', 'tag_group_id'
|
|
||||||
node[kind] = integer_id(record, node.fetch(kind), :placements)
|
|
||||||
if node['description'].present?
|
|
||||||
invalid!(record, :placements, 'Tag / Tag Group Placement に説明文は指定できません.')
|
|
||||||
end
|
|
||||||
node.delete('description')
|
|
||||||
when 'section_name'
|
|
||||||
section_name = node.fetch(kind)
|
|
||||||
unless section_name.is_a?(String) && section_name.strip.present? &&
|
|
||||||
section_name.length <= 255
|
|
||||||
invalid!(record, :placements, 'Section 名は 1〜255 文字で入力してください.')
|
|
||||||
end
|
|
||||||
node[kind] = section_name.strip
|
|
||||||
unless node['description'].nil? || node['description'].is_a?(String)
|
|
||||||
invalid!(record, :placements, 'Section の説明文は文字列で入力してください.')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
node['children'] = normalize_placement_nodes(record, children, depth + 1)
|
|
||||||
node
|
|
||||||
end
|
|
||||||
end
|
|
||||||
private_class_method :normalize_placement_nodes
|
|
||||||
|
|
||||||
def self.validate_references!(record, members, placements)
|
|
||||||
membership = members.to_set
|
|
||||||
placed = placement_references(placements).uniq
|
|
||||||
missing = placed - membership.to_a
|
|
||||||
invalid!(record, :placements, 'Placement の対象は membership に含めてください.') if
|
|
||||||
missing.any?
|
|
||||||
|
|
||||||
tag_ids = members.filter_map { _1['tag_id'] }
|
|
||||||
group_ids = members.filter_map { _1['tag_group_id'] }
|
|
||||||
tags = Tag.where(id: tag_ids).index_by(&:id)
|
|
||||||
groups = TagGroup.where(id: group_ids).index_by(&:id)
|
|
||||||
invalid!(record, :members, '存在しない Tag が含まれています.') if
|
|
||||||
tags.size != tag_ids.uniq.size
|
|
||||||
invalid!(record, :members, '存在しない Tag Group が含まれています.') if
|
|
||||||
groups.size != group_ids.uniq.size
|
|
||||||
|
|
||||||
tag_versions = TagVersion.where(tag_id: tag_ids).pluck(:tag_id, :version_no).to_set
|
|
||||||
group_versions = TagGroupVersion.where(tag_group_id: group_ids)
|
|
||||||
.pluck(:tag_group_id, :version_no).to_set
|
|
||||||
tag_names = TagName.where(tag_id: tag_ids, language_code: 'ja', primary_flg: true)
|
|
||||||
.pluck(:tag_id).uniq
|
|
||||||
|
|
||||||
members.each do |member|
|
|
||||||
if member.key?('tag_id')
|
|
||||||
id = member.fetch('tag_id')
|
|
||||||
invalid!(record, :members, "Tag ##{ id } に日本語の正規名がありません.") unless
|
|
||||||
tag_names.include?(id)
|
|
||||||
version_no = tags.fetch(id).version_no
|
|
||||||
unless tag_versions.include?([id, version_no])
|
|
||||||
invalid!(record, :members, "Tag ##{ id } の Version がありません.")
|
|
||||||
end
|
|
||||||
member['version_no'] = version_no
|
|
||||||
else
|
|
||||||
id = member.fetch('tag_group_id')
|
|
||||||
version_no = groups.fetch(id).version_no
|
|
||||||
unless group_versions.include?([id, version_no])
|
|
||||||
invalid!(record, :members, "Tag Group ##{ id } の Version がありません.")
|
|
||||||
end
|
|
||||||
member['version_no'] = version_no
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
private_class_method :validate_references!
|
|
||||||
|
|
||||||
def self.validate_containment!(record, members)
|
|
||||||
target_id = record.id
|
|
||||||
return unless target_id
|
|
||||||
|
|
||||||
children = members.filter_map { _1['tag_group_id'] }
|
|
||||||
edges = TagGroupGroup.pluck(:tag_group_id, :child_tag_group_id)
|
|
||||||
.group_by(&:first).transform_values { _1.map(&:last) }
|
|
||||||
edges[target_id] = children
|
|
||||||
visiting = Set.new
|
|
||||||
visited = Set.new
|
|
||||||
visit = lambda do |group_id|
|
|
||||||
invalid!(record, :members, 'Tag Group の循環包含はできません.') if
|
|
||||||
group_id == target_id && visiting.include?(group_id)
|
|
||||||
return if visited.include?(group_id)
|
|
||||||
|
|
||||||
visiting.add(group_id)
|
|
||||||
edges.fetch(group_id, []).each do |child_id|
|
|
||||||
invalid!(record, :members, 'Tag Group の循環包含はできません.') if
|
|
||||||
child_id == target_id
|
|
||||||
visit.call(child_id)
|
|
||||||
end
|
|
||||||
visiting.delete(group_id)
|
|
||||||
visited.add(group_id)
|
|
||||||
end
|
|
||||||
children.each { visit.call(_1) }
|
|
||||||
end
|
|
||||||
private_class_method :validate_containment!
|
|
||||||
|
|
||||||
def self.persist_structure!(group:, attributes:, actor:)
|
|
||||||
tag_members = TagGroupTag.where(tag_group_id: group.id).index_by(&:tag_id)
|
|
||||||
group_members = TagGroupGroup.where(tag_group_id: group.id).index_by(&:child_tag_group_id)
|
|
||||||
attributes.fetch(:members).each do |member|
|
|
||||||
if member.key?('tag_id')
|
|
||||||
next if tag_members.delete(member.fetch('tag_id'))
|
|
||||||
|
|
||||||
TagGroupTag.create!(tag_group_id: group.id, tag_id: member.fetch('tag_id'),
|
|
||||||
created_by_user: actor)
|
|
||||||
else
|
|
||||||
next if group_members.delete(member.fetch('tag_group_id'))
|
|
||||||
|
|
||||||
TagGroupGroup.create!(tag_group_id: group.id,
|
|
||||||
child_tag_group_id: member.fetch('tag_group_id'),
|
|
||||||
created_by_user: actor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
existing = TagGroupPlacement.where(tag_group_id: group.id).index_by(&:position)
|
|
||||||
persist_placements!(group:, nodes: attributes.fetch(:placements), actor:, existing:)
|
|
||||||
existing.values.sort_by(&:position).reverse_each(&:delete)
|
|
||||||
tag_members.each_value(&:delete)
|
|
||||||
group_members.each_value(&:delete)
|
|
||||||
end
|
|
||||||
private_class_method :persist_structure!
|
|
||||||
|
|
||||||
def self.persist_placements!(group:, nodes:, actor:, existing:, parent_position: nil,
|
|
||||||
next_position: [0])
|
|
||||||
nodes.each do |node|
|
|
||||||
next_position[0] += 1
|
|
||||||
position = next_position[0]
|
|
||||||
row = existing.delete(position) || TagGroupPlacement.new(
|
|
||||||
tag_group_id: group.id, position:, created_by_user: actor)
|
|
||||||
row.assign_attributes(
|
|
||||||
parent_position:,
|
|
||||||
section_name: node['section_name'], description: node['description'],
|
|
||||||
tag_id: node['tag_id'], child_tag_group_id: node['tag_group_id'])
|
|
||||||
if row.new_record? || row.changed?
|
|
||||||
row.updated_by_user = actor
|
|
||||||
row.save!
|
|
||||||
end
|
|
||||||
persist_placements!(group:, nodes: node.fetch('children'), actor:, existing:,
|
|
||||||
parent_position: position, next_position:)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
private_class_method :persist_placements!
|
|
||||||
|
|
||||||
def self.persist_version!(group:, attributes:, actor:, event_type:)
|
|
||||||
TagGroupVersion.create!(tag_group: group, version_no: group.version_no,
|
|
||||||
event_type:, name: group.name,
|
|
||||||
description: group.description,
|
|
||||||
members: attributes.fetch(:members),
|
|
||||||
placements: attributes.fetch(:placements),
|
|
||||||
created_by_user: actor)
|
|
||||||
end
|
|
||||||
private_class_method :persist_version!
|
|
||||||
|
|
||||||
def self.sync_compatibility!(group:, actor:, old_name:,
|
|
||||||
require_compatibility:, compatibility_body:)
|
|
||||||
representation = TagGroupRepr.one(group)
|
|
||||||
body = begin
|
|
||||||
TagGroupMarkdownRenderer.render(representation)
|
|
||||||
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
|
|
||||||
if compatibility_body.present?
|
|
||||||
compatibility_body
|
|
||||||
elsif require_compatibility
|
|
||||||
invalid!(group, :base, error.message)
|
|
||||||
else
|
|
||||||
return error.message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
invalid!(group, :base, '本文は必須です.') if body.blank?
|
|
||||||
|
|
||||||
title = "#{ PREFIX }#{ group.name }"
|
|
||||||
old_title = old_name && "#{ PREFIX }#{ old_name }"
|
|
||||||
page = if old_title
|
|
||||||
WikiPage.joins(:tag_name).find_by(tag_names: { name: old_title })
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
page ||= WikiPage.joins(:tag_name).find_by(tag_names: { name: title })
|
|
||||||
|
|
||||||
if page
|
|
||||||
tag_name = page.tag_name
|
|
||||||
tag_name.update!(name: title) if tag_name.name != title
|
|
||||||
Wiki::Commit.legacy_content!(page:, body:, created_user: actor, message: nil,
|
|
||||||
base_revision_id: nil)
|
|
||||||
else
|
|
||||||
tag_name = TagName.find_or_initialize_by(language_code: 'ja', name: title)
|
|
||||||
tag_name.script_code = 'Jpan'
|
|
||||||
tag_name.primary_flg = true
|
|
||||||
tag_name.auto_generated = false
|
|
||||||
tag_name.save!
|
|
||||||
Wiki::Commit.create_legacy_content!(tag_name:, body:, created_by_user: actor)
|
|
||||||
end
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
private_class_method :sync_compatibility!
|
|
||||||
|
|
||||||
def self.report_sync_issue(group, actor, reason)
|
|
||||||
return unless reason
|
|
||||||
|
|
||||||
payload = { status: 'unsupported_structure', tag_group_id: group.id,
|
|
||||||
version_no: group.version_no, user_id: actor&.id, reason: }
|
|
||||||
Rails.logger.warn("tag_group_wiki_sync_skipped #{ payload.to_json }")
|
|
||||||
end
|
|
||||||
private_class_method :report_sync_issue
|
|
||||||
|
|
||||||
def self.placement_references(nodes)
|
|
||||||
nodes.flat_map do |node|
|
|
||||||
own = if node.key?('tag_id')
|
|
||||||
[{ 'tag_id' => node.fetch('tag_id') }]
|
|
||||||
elsif node.key?('tag_group_id')
|
|
||||||
[{ 'tag_group_id' => node.fetch('tag_group_id') }]
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
own + placement_references(node.fetch('children'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
private_class_method :placement_references
|
|
||||||
|
|
||||||
def self.stringify_keys(value)
|
|
||||||
case value
|
|
||||||
when ActionController::Parameters
|
|
||||||
value.to_unsafe_h.deep_stringify_keys
|
|
||||||
when Hash
|
|
||||||
value.deep_stringify_keys
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
private_class_method :stringify_keys
|
|
||||||
|
|
||||||
def self.integer_id(record, value, attribute)
|
|
||||||
integer = if value.is_a?(Integer)
|
|
||||||
value
|
|
||||||
elsif value.is_a?(String) && value.match?(/\A[0-9]+\z/)
|
|
||||||
value.to_i
|
|
||||||
end
|
|
||||||
invalid!(record, attribute, 'ID は正の整数で指定してください.') unless integer
|
|
||||||
invalid!(record, attribute, 'ID は正の整数で指定してください.') unless integer.positive?
|
|
||||||
integer
|
|
||||||
end
|
|
||||||
private_class_method :integer_id
|
|
||||||
|
|
||||||
def self.invalid!(record, attribute, message)
|
|
||||||
record.errors.add(attribute, message)
|
|
||||||
raise ActiveRecord::RecordInvalid, record
|
|
||||||
end
|
|
||||||
private_class_method :invalid!
|
|
||||||
end
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
class TagVersionRecorder < VersionRecorder
|
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!
|
new(tag:, event_type:, created_by_user:).record!
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -14,29 +14,10 @@ class TagVersionRecorder < VersionRecorder
|
|||||||
def record_key = :tag
|
def record_key = :tag
|
||||||
|
|
||||||
def snapshot_attributes
|
def snapshot_attributes
|
||||||
names = snapshot_names
|
{ name: @record.name,
|
||||||
compatibility_names = names.fetch('ja')
|
|
||||||
|
|
||||||
{ name: compatibility_names.fetch('name'),
|
|
||||||
category: @record.category,
|
category: @record.category,
|
||||||
deprecated_at: @record.deprecated_at,
|
deprecated_at: @record.deprecated_at,
|
||||||
aliases: compatibility_names.fetch('aliases').map { _1.fetch('name') }.join(' '),
|
aliases: @record.snapshot_aliases.join(' '),
|
||||||
names:,
|
|
||||||
parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') }
|
parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') }
|
||||||
end
|
end
|
||||||
|
|
||||||
def snapshot_names
|
|
||||||
@record.tag_names.order(:language_code, :name).group_by(&:language_code)
|
|
||||||
.transform_values do |tag_names|
|
|
||||||
primary = tag_names.find(&:primary?)
|
|
||||||
aliases = tag_names.reject(&:primary?).sort_by(&:name).map { |tag_name|
|
|
||||||
{ 'name' => tag_name.name, 'script_code' => tag_name.script_code }
|
|
||||||
}
|
|
||||||
|
|
||||||
{ 'name' => primary.name,
|
|
||||||
'script_code' => primary.script_code,
|
|
||||||
'auto_generated' => primary.auto_generated,
|
|
||||||
'aliases' => aliases }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
class TagVersioning
|
class TagVersioning
|
||||||
def self.record! tag, event_type:, created_by_user:, language_code: 'ja'
|
def self.record! tag, event_type:, created_by_user:
|
||||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
|
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:
|
||||||
return if tag.tag_versions.exists?
|
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'
|
def self.record_tag_snapshot! tag, created_by_user:
|
||||||
event_type = tag.tag_versions.exists? ? :update : :create
|
event_type = tag.tag_versions.exists? ? :update : :create
|
||||||
record!(tag, event_type:, created_by_user:, language_code:)
|
record!(tag, event_type:, created_by_user:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.record_tag_snapshots! tags, created_by_user:, language_code: 'ja'
|
def self.record_tag_snapshots! tags, created_by_user:
|
||||||
tags.each do |tag|
|
tags.each do |tag|
|
||||||
record_tag_snapshot!(tag, created_by_user:, language_code:)
|
record_tag_snapshot!(tag, created_by_user:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class TheatrePostSelector
|
|||||||
def weighted_candidates
|
def weighted_candidates
|
||||||
@weighted_candidates ||= begin
|
@weighted_candidates ||= begin
|
||||||
penalties = tag_penalties
|
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|
|
posts.map do |post|
|
||||||
post_tags = post.tags.to_a
|
post_tags = post.tags.to_a
|
||||||
@@ -82,7 +82,7 @@ class TheatrePostSelector
|
|||||||
def tag_penalty_json
|
def tag_penalty_json
|
||||||
return [] if tag_penalties.empty?
|
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
|
tag_penalties
|
||||||
.map { |tag_id, penalty|
|
.map { |tag_id, penalty|
|
||||||
@@ -113,7 +113,7 @@ class TheatrePostSelector
|
|||||||
|
|
||||||
def light_tag_json tag
|
def light_tag_json tag
|
||||||
{ id: tag.id,
|
{ id: tag.id,
|
||||||
name: tag.name('ja'),
|
name: tag.name,
|
||||||
category: tag.category }
|
category: tag.category }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,15 +8,6 @@ module Wiki
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.create_content! tag_name:, body:, created_by_user:, message: nil
|
def self.create_content! tag_name:, body:, created_by_user:, message: nil
|
||||||
if tag_name.tag && tag_name.language_code == 'ja'
|
|
||||||
return TagDescriptionOperation.create!(
|
|
||||||
tag: tag_name.tag, body:, created_by_user:, message:)
|
|
||||||
end
|
|
||||||
|
|
||||||
create_legacy_content!(tag_name:, body:, created_by_user:, message:)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.create_legacy_content! tag_name:, body:, created_by_user:, message: nil
|
|
||||||
normalised = normalise_body(body)
|
normalised = normalise_body(body)
|
||||||
|
|
||||||
page = WikiPage.new(tag_name:,
|
page = WikiPage.new(tag_name:,
|
||||||
@@ -42,18 +33,6 @@ module Wiki
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
|
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
|
||||||
description = TagDescription.find_by(id: page.id)
|
|
||||||
if description
|
|
||||||
return TagDescriptionOperation.update!(
|
|
||||||
tag: description.tag, body:, updated_by_user: created_user,
|
|
||||||
message:, base_revision_id:)
|
|
||||||
end
|
|
||||||
|
|
||||||
legacy_content!(page:, body:, created_user:, message:, base_revision_id:)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.legacy_content! page:, body:, created_user:, message: nil,
|
|
||||||
base_revision_id: nil
|
|
||||||
new(page:, created_user:).content!(body:, message:, base_revision_id:)
|
new(page:, created_user:).content!(body:, message:, base_revision_id:)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -78,6 +57,9 @@ module Wiki
|
|||||||
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
|
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
|
||||||
tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))
|
tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))
|
||||||
|
|
||||||
|
line_id_by_sha = upsert_lines!(lines, line_shas)
|
||||||
|
line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@page.lock!
|
@page.lock!
|
||||||
|
|
||||||
@@ -90,8 +72,6 @@ module Wiki
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
line_id_by_sha = upsert_lines!(lines, line_shas)
|
|
||||||
line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }
|
|
||||||
@page.update!(body: normalised)
|
@page.update!(body: normalised)
|
||||||
|
|
||||||
WikiVersionRecorder.record!(
|
WikiVersionRecorder.record!(
|
||||||
@@ -114,6 +94,7 @@ module Wiki
|
|||||||
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
|
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
|
||||||
end
|
end
|
||||||
WikiRevisionLine.insert_all!(rows) if rows.any?
|
WikiRevisionLine.insert_all!(rows) if rows.any?
|
||||||
|
|
||||||
rev
|
rev
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
module Wiki
|
|
||||||
class CompatibilityWriter
|
|
||||||
def self.create! tag_name:, body:, created_by_user:, message: nil
|
|
||||||
Commit.create_legacy_content!(
|
|
||||||
tag_name:, body:, created_by_user:, message:)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.update! page:, body:, updated_by_user:, message: nil,
|
|
||||||
base_revision_id: nil
|
|
||||||
normalised = Commit.normalise_body(body)
|
|
||||||
if normalised.blank?
|
|
||||||
return Commit.legacy_content!(
|
|
||||||
page:, body: normalised, created_user: updated_by_user, message:,
|
|
||||||
base_revision_id:)
|
|
||||||
end
|
|
||||||
|
|
||||||
if base_revision_id.present?
|
|
||||||
current_id = page.wiki_revisions.maximum(:id)
|
|
||||||
if current_id && current_id != base_revision_id.to_i
|
|
||||||
raise Commit::Conflict,
|
|
||||||
"競合が発生してゐます" +
|
|
||||||
"(現在の Id.:#{ current_id },ベース Id.:#{ base_revision_id })."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return if Commit.normalise_body(page.body) == normalised
|
|
||||||
|
|
||||||
Commit.legacy_content!(
|
|
||||||
page:, body: normalised, created_user: updated_by_user, message:,
|
|
||||||
base_revision_id:)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
resources :tag_groups, only: [:index, :show, :create, :update]
|
|
||||||
|
|
||||||
resources :nico_tags, path: 'tags/nico', only: [:index, :show, :update]
|
resources :nico_tags, path: 'tags/nico', only: [:index, :show, :update]
|
||||||
|
|
||||||
scope 'tags/:parent_id/children', controller: :tag_children do
|
scope 'tags/:parent_id/children', controller: :tag_children do
|
||||||
@@ -16,7 +14,6 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
scope :name do
|
scope :name do
|
||||||
get ':name/deerjikists', action: :deerjikists_by_name
|
get ':name/deerjikists', action: :deerjikists_by_name
|
||||||
get ':name/description', to: 'tag_descriptions#show_by_title'
|
|
||||||
get ':name/materials', action: :materials_by_name
|
get ':name/materials', action: :materials_by_name
|
||||||
get ':name', action: :show_by_name
|
get ':name', action: :show_by_name
|
||||||
end
|
end
|
||||||
@@ -28,9 +25,6 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
get :deerjikists
|
get :deerjikists
|
||||||
put :deerjikists, action: :update_deerjikists
|
put :deerjikists, action: :update_deerjikists
|
||||||
|
|
||||||
get :description, to: 'tag_descriptions#show'
|
|
||||||
put :description, to: 'tag_descriptions#update'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,7 +35,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :wiki_pages, path: 'wiki', only: [:index, :show, :create, :update] do
|
resources :wiki_pages, path: 'wiki', only: [:index, :show, :create, :update] do
|
||||||
collection do
|
collection do
|
||||||
get :search, action: :index
|
get :search
|
||||||
get :changes
|
get :changes
|
||||||
|
|
||||||
scope :title do
|
scope :title do
|
||||||
|
|||||||
@@ -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,23 +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
|
|
||||||
|
|
||||||
t.check_constraint "REGEXP_LIKE(code, '^[A-Z][a-z]{3}$', 'c')",
|
|
||||||
name: 'scripts_code_check'
|
|
||||||
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 CreateRegions < ActiveRecord::Migration[8.0]
|
|
||||||
def up
|
|
||||||
create_table :regions, id: { type: :integer, limit: 2 }, primary_key: :code do |t|
|
|
||||||
t.column :alpha_code, 'CHAR(2)'
|
|
||||||
t.string :name, null: false
|
|
||||||
t.datetime :deprecated_at, index: true
|
|
||||||
t.datetime :created_at, null: false
|
|
||||||
|
|
||||||
t.check_constraint "REGEXP_LIKE(alpha_code, '^[A-Z]{2}$', 'c')",
|
|
||||||
name: 'regions_alpha_code_check'
|
|
||||||
t.index [:alpha_code], unique: true
|
|
||||||
end
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
INSERT INTO
|
|
||||||
regions(code, alpha_code, name, created_at)
|
|
||||||
VALUES
|
|
||||||
(392, 'JP', '日本国', #{ connection.quote Time.current })
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
drop_table :regions
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,38 +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.integer :region_code, limit: 2, null: false
|
|
||||||
t.string :variants, limit: 255, null: false, default: ''
|
|
||||||
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
|
|
||||||
t.foreign_key :regions, column: :region_code, primary_key: :code
|
|
||||||
t.check_constraint ('REGEXP_LIKE(variants, ' +
|
|
||||||
"'^(" +
|
|
||||||
'(([a-z0-9]{5,8}|[0-9][a-z0-9]{3})' +
|
|
||||||
'(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*' +
|
|
||||||
'(-x(-[a-z0-9]{1,8})+)?)' +
|
|
||||||
'|(x(-[a-z0-9]{1,8})+)' +
|
|
||||||
")?$', 'c')"),
|
|
||||||
name: 'locales_variants_check'
|
|
||||||
|
|
||||||
t.index [:language_code, :script_code, :region_code, :variants], unique: true
|
|
||||||
end
|
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
INSERT INTO
|
|
||||||
locales(code, language_code, script_code, region_code, name, created_at)
|
|
||||||
VALUES
|
|
||||||
('ja', 'ja', 'Jpan', 392, '日本語', #{ connection.quote Time.current })
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
drop_table :locales
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,69 +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_column :tag_names, :auto_generated, :boolean,
|
|
||||||
null: false, default: false, after: :primary_flg
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
add_check_constraint :tag_names,
|
|
||||||
'primary_flg OR (NOT auto_generated)',
|
|
||||||
name: 'chk_tag_names_auto_generated_primary'
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
remove_check_constraint :tag_names,
|
|
||||||
name: 'chk_tag_names_auto_generated_primary'
|
|
||||||
|
|
||||||
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, :auto_generated
|
|
||||||
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,68 +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|
|
|
||||||
aliases = version.aliases.to_s.split.filter_map do |name|
|
|
||||||
name.present? && { name:, script_code: 'Jpan' }
|
|
||||||
end
|
|
||||||
names = { ja: { name: version.name,
|
|
||||||
script_code: 'Jpan',
|
|
||||||
auto_generated: false,
|
|
||||||
aliases: } }
|
|
||||||
version.update_columns(names:)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
add_names_constraint!
|
|
||||||
|
|
||||||
change_column_null :tag_versions, :names, false
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
remove_check_constraint :tag_versions,
|
|
||||||
name: 'chk_tag_versions_names_schema'
|
|
||||||
|
|
||||||
remove_column :tag_versions, :names
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def add_names_constraint!
|
|
||||||
schema = { type: 'object',
|
|
||||||
minProperties: 1,
|
|
||||||
patternProperties: { '^.{1,16}$': name_schema },
|
|
||||||
additionalProperties: false }
|
|
||||||
|
|
||||||
quoted_schema = connection.quote(JSON.generate(schema))
|
|
||||||
|
|
||||||
add_check_constraint :tag_versions,
|
|
||||||
"JSON_SCHEMA_VALID(#{ quoted_schema }, names)",
|
|
||||||
name: 'chk_tag_versions_names_schema'
|
|
||||||
end
|
|
||||||
|
|
||||||
def name_schema
|
|
||||||
{ type: 'object',
|
|
||||||
required: ['name', 'script_code', 'auto_generated', 'aliases'],
|
|
||||||
properties: { **name_schema_base,
|
|
||||||
auto_generated: { type: 'boolean' },
|
|
||||||
aliases: { type: 'array',
|
|
||||||
uniqueItems: true,
|
|
||||||
items: { type: 'object',
|
|
||||||
required: ['name', 'script_code'],
|
|
||||||
properties: name_schema_base,
|
|
||||||
additionalProperties: false } } },
|
|
||||||
additionalProperties: false }
|
|
||||||
end
|
|
||||||
|
|
||||||
def name_schema_base
|
|
||||||
{ name: { type: 'string', minLength: 1, maxLength: 255 },
|
|
||||||
script_code: { type: 'string', pattern: '^[A-Z][a-z]{3}$' } }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
class CreateTagDescriptions < ActiveRecord::Migration[8.0]
|
|
||||||
def change
|
|
||||||
create_table :tag_description_versions do |t|
|
|
||||||
t.references :tag, null: false, foreign_key: false
|
|
||||||
t.string :language_code, limit: 16, null: false
|
|
||||||
t.integer :version_no, null: false
|
|
||||||
t.string :event_type, null: false
|
|
||||||
t.text :body, null: false
|
|
||||||
t.string :source_language_code, limit: 16
|
|
||||||
t.integer :source_version_no
|
|
||||||
t.references :created_by_user
|
|
||||||
t.datetime :created_at, null: false
|
|
||||||
|
|
||||||
t.index [:tag_id, :language_code, :version_no], unique: true
|
|
||||||
t.foreign_key :tag_description_versions,
|
|
||||||
column: [:tag_id, :source_language_code, :source_version_no],
|
|
||||||
primary_key: [:tag_id, :language_code, :version_no]
|
|
||||||
|
|
||||||
t.check_constraint 'version_no > 0',
|
|
||||||
name: 'chk_tag_description_versions_version_no_positive'
|
|
||||||
|
|
||||||
t.check_constraint "event_type IN ('create', 'update', 'discard', 'restore')",
|
|
||||||
name: 'tag_description_versions_event_type_valid'
|
|
||||||
|
|
||||||
t.check_constraint ('(source_language_code IS NULL AND source_version_no IS NULL) OR ' +
|
|
||||||
'(source_language_code IS NOT NULL AND source_version_no IS NOT NULL)'),
|
|
||||||
name: 'chk_tag_description_versions_source_pair'
|
|
||||||
|
|
||||||
t.check_constraint 'source_language_code IS NULL OR source_language_code <> language_code',
|
|
||||||
name: 'chk_tag_description_versions_source_language_code_different'
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table :tag_descriptions do |t|
|
|
||||||
t.references :tag, null: false, foreign_key: true
|
|
||||||
t.string :language_code, limit: 16, null: false, index: true
|
|
||||||
t.integer :version_no, null: false
|
|
||||||
t.text :body, null: false
|
|
||||||
t.string :source_language_code, limit: 16
|
|
||||||
t.integer :source_version_no
|
|
||||||
t.references :created_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.references :updated_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.timestamps
|
|
||||||
|
|
||||||
t.foreign_key :languages, column: :language_code, primary_key: :code
|
|
||||||
t.foreign_key :tag_description_versions,
|
|
||||||
column: [:tag_id, :source_language_code, :source_version_no],
|
|
||||||
primary_key: [:tag_id, :language_code, :version_no]
|
|
||||||
|
|
||||||
t.index [:tag_id, :language_code], unique: true
|
|
||||||
t.index [:source_language_code, :source_version_no]
|
|
||||||
|
|
||||||
t.check_constraint 'version_no > 0',
|
|
||||||
name: 'chk_tag_descriptions_version_no_positive'
|
|
||||||
|
|
||||||
t.check_constraint ('(source_language_code IS NULL AND source_version_no IS NULL) OR ' +
|
|
||||||
'(source_language_code IS NOT NULL AND source_version_no IS NOT NULL)'),
|
|
||||||
name: 'chk_tag_descriptions_source_pair'
|
|
||||||
|
|
||||||
t.check_constraint 'source_language_code IS NULL OR source_language_code <> language_code',
|
|
||||||
name: 'chk_tag_descriptions_source_language_code_different'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
class MigrateTagDescriptionsFromWikiPages < ActiveRecord::Migration[8.0]
|
|
||||||
class WikiPage < ActiveRecord::Base
|
|
||||||
self.table_name = 'wiki_pages'
|
|
||||||
end
|
|
||||||
|
|
||||||
class WikiRevision < ActiveRecord::Base
|
|
||||||
self.table_name = 'wiki_revisions'
|
|
||||||
end
|
|
||||||
|
|
||||||
class WikiRevisionLine < ActiveRecord::Base
|
|
||||||
self.table_name = 'wiki_revision_lines'
|
|
||||||
end
|
|
||||||
|
|
||||||
class TagName < ActiveRecord::Base
|
|
||||||
self.table_name = 'tag_names'
|
|
||||||
end
|
|
||||||
|
|
||||||
class TagDescription < ActiveRecord::Base
|
|
||||||
self.table_name = 'tag_descriptions'
|
|
||||||
end
|
|
||||||
|
|
||||||
class TagDescriptionVersion < ActiveRecord::Base
|
|
||||||
self.table_name = 'tag_description_versions'
|
|
||||||
end
|
|
||||||
|
|
||||||
def up
|
|
||||||
WikiPage.find_each do |page|
|
|
||||||
tag_name = TagName.find_by(id: page.tag_name_id, language_code: 'ja')
|
|
||||||
next unless tag_name&.tag_id
|
|
||||||
|
|
||||||
revisions = WikiRevision.where(wiki_page_id: page.id).order(:id).to_a
|
|
||||||
|
|
||||||
revisions.each_with_index do |revision, index|
|
|
||||||
version_no = index + 1
|
|
||||||
|
|
||||||
tag_id = tag_name[:tag_id]
|
|
||||||
|
|
||||||
unless tag_id
|
|
||||||
raise (
|
|
||||||
'tag_id is nil: ' +
|
|
||||||
"wiki_page_id=#{ page.id }, " +
|
|
||||||
"tag_name_id=#{ tag_name.id }, " +
|
|
||||||
"tag_name=#{ tag_name.attributes.inspect}")
|
|
||||||
end
|
|
||||||
|
|
||||||
revision_id = revision.id
|
|
||||||
|
|
||||||
say (
|
|
||||||
"wiki_page_id=#{ page.id }, " +
|
|
||||||
"tag_name_id=#{ tag_name.id }, " +
|
|
||||||
"tag_id=#{ tag_id }, " +
|
|
||||||
"revision_id=#{ revision_id }")
|
|
||||||
|
|
||||||
version = TagDescriptionVersion.new(
|
|
||||||
id: revision_id,
|
|
||||||
tag_id:,
|
|
||||||
language_code: 'ja',
|
|
||||||
version_no:,
|
|
||||||
event_type: version_no == 1 ? 'create' : 'update',
|
|
||||||
body: revision_body(revision_id),
|
|
||||||
source_language_code: nil,
|
|
||||||
source_version_no: nil,
|
|
||||||
created_by_user_id: revision.created_user_id,
|
|
||||||
created_at: revision.created_at)
|
|
||||||
|
|
||||||
say version.attributes.inspect
|
|
||||||
|
|
||||||
version.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
next if page.discarded_at.present?
|
|
||||||
next if revisions.empty?
|
|
||||||
|
|
||||||
TagDescription.create!(
|
|
||||||
id: page.id,
|
|
||||||
tag_id: tag_name.tag_id,
|
|
||||||
language_code: 'ja',
|
|
||||||
version_no: revisions.length,
|
|
||||||
body: page.body,
|
|
||||||
source_language_code: nil,
|
|
||||||
source_version_no: nil,
|
|
||||||
created_by_user_id: page.created_user_id,
|
|
||||||
updated_by_user_id: page.updated_user_id,
|
|
||||||
created_at: page.created_at,
|
|
||||||
updated_at: page.updated_at)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def revision_body revision_id
|
|
||||||
WikiRevisionLine
|
|
||||||
.joins('INNER JOIN wiki_lines ' +
|
|
||||||
'ON wiki_lines.id = wiki_revision_lines.wiki_line_id')
|
|
||||||
.where(wiki_revision_id: revision_id)
|
|
||||||
.order(:position)
|
|
||||||
.pluck('wiki_lines.body')
|
|
||||||
.join("\n")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
class RepairWikiUpdatedAt < ActiveRecord::Migration[8.0]
|
|
||||||
class WikiPage < ActiveRecord::Base
|
|
||||||
self.table_name = 'wiki_pages'
|
|
||||||
end
|
|
||||||
|
|
||||||
class WikiVersion < ActiveRecord::Base
|
|
||||||
self.table_name = 'wiki_versions'
|
|
||||||
end
|
|
||||||
|
|
||||||
class TagDescription < ActiveRecord::Base
|
|
||||||
self.table_name = 'tag_descriptions'
|
|
||||||
end
|
|
||||||
|
|
||||||
class TagDescriptionVersion < ActiveRecord::Base
|
|
||||||
self.table_name = 'tag_description_versions'
|
|
||||||
end
|
|
||||||
|
|
||||||
def up
|
|
||||||
say_with_time 'Repairing wiki_pages.updated_at from wiki_versions' do
|
|
||||||
WikiPage.find_each do |page|
|
|
||||||
version =
|
|
||||||
WikiVersion
|
|
||||||
.where(wiki_page_id: page.id)
|
|
||||||
.order(version_no: :desc)
|
|
||||||
.first
|
|
||||||
|
|
||||||
next unless version
|
|
||||||
|
|
||||||
page.update_columns(updated_at: version.created_at)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
say_with_time 'Repairing tag_descriptions.updated_at from tag_description_versions' do
|
|
||||||
TagDescription.find_each do |description|
|
|
||||||
version =
|
|
||||||
TagDescriptionVersion
|
|
||||||
.where(
|
|
||||||
tag_id: description.tag_id,
|
|
||||||
language_code: description.language_code)
|
|
||||||
.order(version_no: :desc)
|
|
||||||
.first
|
|
||||||
|
|
||||||
next unless version
|
|
||||||
|
|
||||||
description.update_columns(updated_at: version.created_at)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
class CreateTagGroups < ActiveRecord::Migration[8.0]
|
|
||||||
def change
|
|
||||||
# 翻訳系は上流の Translation リソースが未完のため保留
|
|
||||||
|
|
||||||
create_table :tag_groups do |t|
|
|
||||||
t.integer :version_no, null: false
|
|
||||||
t.string :name, limit: 255, null: false
|
|
||||||
t.text :description
|
|
||||||
t.references :created_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.references :updated_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.timestamps
|
|
||||||
|
|
||||||
t.check_constraint 'version_no > 0', name: 'chk_tag_groups_version_no_positive'
|
|
||||||
t.index :name, unique: true
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table :tag_group_tags, primary_key: [:tag_group_id, :tag_id] do |t|
|
|
||||||
t.references :tag_group, null: false, foreign_key: true, index: false
|
|
||||||
t.references :tag, null: false, foreign_key: true
|
|
||||||
t.references :created_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.datetime :created_at, null: false
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table :tag_group_groups, primary_key: [:tag_group_id, :child_tag_group_id] do |t|
|
|
||||||
t.references :tag_group, null: false, foreign_key: true, index: false
|
|
||||||
t.references :child_tag_group,
|
|
||||||
null: false, foreign_key: { to_table: :tag_groups }
|
|
||||||
t.references :created_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.datetime :created_at, null: false
|
|
||||||
|
|
||||||
t.check_constraint 'tag_group_id <> child_tag_group_id',
|
|
||||||
name: 'chk_tag_group_groups_tag_group_id_not_child_tag_group_id'
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table :tag_group_placements, primary_key: [:tag_group_id, :position] do |t|
|
|
||||||
t.references :tag_group, null: false, foreign_key: true, index: false
|
|
||||||
t.integer :position, null: false
|
|
||||||
t.integer :parent_position
|
|
||||||
t.string :section_name, limit: 255
|
|
||||||
t.text :description
|
|
||||||
t.references :tag
|
|
||||||
t.references :child_tag_group
|
|
||||||
t.references :created_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.references :updated_by_user, foreign_key: { to_table: :users }
|
|
||||||
t.timestamps
|
|
||||||
|
|
||||||
t.foreign_key :tag_group_placements,
|
|
||||||
column: [:tag_group_id, :parent_position],
|
|
||||||
primary_key: [:tag_group_id, :position]
|
|
||||||
|
|
||||||
t.foreign_key :tag_group_tags,
|
|
||||||
column: [:tag_group_id, :tag_id],
|
|
||||||
primary_key: [:tag_group_id, :tag_id]
|
|
||||||
|
|
||||||
t.foreign_key :tag_group_groups,
|
|
||||||
column: [:tag_group_id, :child_tag_group_id],
|
|
||||||
primary_key: [:tag_group_id, :child_tag_group_id]
|
|
||||||
|
|
||||||
t.check_constraint 'position > parent_position',
|
|
||||||
name: 'chk_tag_group_placements_position_gt_parent_position'
|
|
||||||
|
|
||||||
t.check_constraint ('((section_name IS NOT NULL AND tag_id IS NULL AND child_tag_group_id IS NULL) OR ' +
|
|
||||||
'(section_name IS NULL AND tag_id IS NOT NULL AND child_tag_group_id IS NULL) OR ' +
|
|
||||||
'(section_name IS NULL AND tag_id IS NULL AND child_tag_group_id IS NOT NULL)) AND ' +
|
|
||||||
'(section_name IS NOT NULL OR description IS NULL)'),
|
|
||||||
name: 'chk_tag_group_placements_references_exclusive'
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table :tag_group_versions do |t|
|
|
||||||
t.references :tag_group, null: false
|
|
||||||
t.integer :version_no, null: false
|
|
||||||
t.string :event_type, null: false
|
|
||||||
t.string :name, limit: 255, null: false
|
|
||||||
t.text :description
|
|
||||||
t.json :members, null: false
|
|
||||||
t.json :placements, null: false
|
|
||||||
t.references :created_by_user
|
|
||||||
t.datetime :created_at, null: false
|
|
||||||
|
|
||||||
t.check_constraint 'version_no > 0',
|
|
||||||
name: 'chk_tag_group_versions_version_no_positive'
|
|
||||||
t.index [:tag_group_id, :version_no], unique: true
|
|
||||||
|
|
||||||
add_members_constraint!(t)
|
|
||||||
add_placements_constraint!(t)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def add_members_constraint! t
|
|
||||||
schema = { type: 'array',
|
|
||||||
uniqueItems: true,
|
|
||||||
items: member_schema }
|
|
||||||
|
|
||||||
quoted_schema = connection.quote(JSON.generate(schema))
|
|
||||||
|
|
||||||
t.check_constraint "JSON_SCHEMA_VALID(#{ quoted_schema }, members)",
|
|
||||||
name: 'chk_tag_group_versions_members_schema'
|
|
||||||
end
|
|
||||||
|
|
||||||
def member_schema
|
|
||||||
{ oneOf: [tag_member_schema, group_member_schema] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_member_schema
|
|
||||||
{ type: 'object',
|
|
||||||
required: ['tag_id', 'version_no'],
|
|
||||||
properties: { tag_id: { type: 'integer', minimum: 1 },
|
|
||||||
version_no: { type: 'integer', minimum: 1 } },
|
|
||||||
additionalProperties: false }
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_member_schema
|
|
||||||
{ type: 'object',
|
|
||||||
required: ['tag_group_id', 'version_no'],
|
|
||||||
properties: { tag_group_id: { type: 'integer', minimum: 1 },
|
|
||||||
version_no: { type: 'integer', minimum: 1 } },
|
|
||||||
additionalProperties: false }
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_placements_constraint! t
|
|
||||||
schema = { type: 'array', items: placement_schema(5) }
|
|
||||||
|
|
||||||
quoted_schema = connection.quote(JSON.generate(schema))
|
|
||||||
|
|
||||||
t.check_constraint "JSON_SCHEMA_VALID(#{ quoted_schema }, placements)",
|
|
||||||
name: 'chk_tag_group_versions_placements_schema'
|
|
||||||
end
|
|
||||||
|
|
||||||
def placement_schema rest
|
|
||||||
{ type: 'object',
|
|
||||||
required: ['children'],
|
|
||||||
properties: { section_name: { type: 'string' },
|
|
||||||
description: { type: ['string', 'null'] },
|
|
||||||
tag_id: { type: 'integer', minimum: 1 },
|
|
||||||
tag_group_id: { type: 'integer', minimum: 1 },
|
|
||||||
children: if rest > 1
|
|
||||||
{ type: 'array', items: placement_schema(rest - 1) }
|
|
||||||
else
|
|
||||||
{ type: 'array', maxItems: 0 }
|
|
||||||
end },
|
|
||||||
oneOf: [{ required: ['section_name'] },
|
|
||||||
{ required: ['tag_id'] },
|
|
||||||
{ required: ['tag_group_id'] }],
|
|
||||||
dependencies: { description: ['section_name'] },
|
|
||||||
additionalProperties: false }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,518 +0,0 @@
|
|||||||
class MigrateTagGroupsFromWikiPages < ActiveRecord::Migration[8.0]
|
|
||||||
PREFIX = 'タググループ:'
|
|
||||||
|
|
||||||
def up
|
|
||||||
require 'json'
|
|
||||||
require 'time'
|
|
||||||
require 'kramdown'
|
|
||||||
require 'kramdown-parser-gfm'
|
|
||||||
|
|
||||||
assert_empty_destination!
|
|
||||||
|
|
||||||
pages = connection.select_all(<<~SQL).to_a
|
|
||||||
SELECT
|
|
||||||
wp.id
|
|
||||||
, wp.created_user_id
|
|
||||||
, wp.updated_user_id
|
|
||||||
, wp.created_at
|
|
||||||
, wp.updated_at
|
|
||||||
, wp.discarded_at
|
|
||||||
, tn.name
|
|
||||||
FROM
|
|
||||||
wiki_pages wp
|
|
||||||
INNER JOIN
|
|
||||||
tag_names tn
|
|
||||||
ON
|
|
||||||
tn.id = wp.tag_name_id
|
|
||||||
WHERE
|
|
||||||
tn.language_code = 'ja'
|
|
||||||
AND tn.name LIKE #{ quote("#{ PREFIX }%") }
|
|
||||||
ORDER BY
|
|
||||||
wp.id
|
|
||||||
SQL
|
|
||||||
|
|
||||||
return if pages.empty?
|
|
||||||
|
|
||||||
pages.each do |page|
|
|
||||||
if page['discarded_at']
|
|
||||||
say "Wiki ##{ page.fetch('id') }: 破棄済みですが,Revision の移行を継続します."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
tag_ids = load_tag_ids
|
|
||||||
page_ids = pages.map { |page| page.fetch('id').to_i }
|
|
||||||
|
|
||||||
revisions = connection.select_all(<<~SQL).to_a
|
|
||||||
SELECT
|
|
||||||
wr.id
|
|
||||||
, wr.wiki_page_id
|
|
||||||
, wr.kind
|
|
||||||
, wr.redirect_page_id
|
|
||||||
, wr.created_user_id
|
|
||||||
, wr.created_at
|
|
||||||
FROM
|
|
||||||
wiki_revisions wr
|
|
||||||
WHERE
|
|
||||||
wr.wiki_page_id IN (#{ page_ids.join(', ') })
|
|
||||||
ORDER BY
|
|
||||||
wr.wiki_page_id
|
|
||||||
, wr.id
|
|
||||||
SQL
|
|
||||||
|
|
||||||
revisions_by_page = revisions.group_by { |row| row.fetch('wiki_page_id').to_i }
|
|
||||||
|
|
||||||
pages = pages.select do |page|
|
|
||||||
page_id = page.fetch('id').to_i
|
|
||||||
if revisions_by_page.fetch(page_id, []).empty?
|
|
||||||
# tag_groups.version_no > 0 のため,Revision 0 件の Wiki は作成しない.
|
|
||||||
say "Wiki ##{ page_id }: Revision が 0 件のため移行対象から除外します."
|
|
||||||
false
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return if pages.empty?
|
|
||||||
|
|
||||||
group_ids = pages.to_h { |page| [page.fetch('name'), page.fetch('id').to_i] }
|
|
||||||
revision_history = pages.to_h do |page|
|
|
||||||
page_id = page.fetch('id').to_i
|
|
||||||
history = revisions_by_page.fetch(page_id).each_with_index.map do |revision, index|
|
|
||||||
{ 'id' => revision.fetch('id').to_i,
|
|
||||||
'version_no' => index + 1,
|
|
||||||
'created_at' => revision.fetch('created_at') }
|
|
||||||
end
|
|
||||||
[page_id, history]
|
|
||||||
end
|
|
||||||
|
|
||||||
tag_history = connection.select_all(<<~SQL).to_a.group_by { |row| row.fetch('tag_id').to_i }
|
|
||||||
SELECT
|
|
||||||
tag_id
|
|
||||||
, version_no
|
|
||||||
, created_at
|
|
||||||
FROM
|
|
||||||
tag_versions
|
|
||||||
ORDER BY
|
|
||||||
tag_id
|
|
||||||
, created_at
|
|
||||||
, version_no
|
|
||||||
SQL
|
|
||||||
|
|
||||||
prepared = { }
|
|
||||||
pages.each do |page|
|
|
||||||
page_id = page.fetch('id').to_i
|
|
||||||
history = revisions_by_page.fetch(page_id)
|
|
||||||
|
|
||||||
prepared[page_id] = history.each_with_index.map do |revision, index|
|
|
||||||
context = "Wiki ##{ page_id } / Revision ##{ revision.fetch('id') }"
|
|
||||||
body = revision_body(revision.fetch('id').to_i)
|
|
||||||
|
|
||||||
if revision.fetch('kind').to_i == 1
|
|
||||||
destination = revision['redirect_page_id']
|
|
||||||
description = "旧 Wiki のリダイレクト先: Wiki ##{ destination }"
|
|
||||||
placements = []
|
|
||||||
members = []
|
|
||||||
say "#{ context }: リダイレクトを説明文として移行します."
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
description, placements = parse_wiki(body, tag_ids, group_ids, context)
|
|
||||||
validate_tree!(placements, context)
|
|
||||||
members = build_members(placements, revision.fetch('created_at'),
|
|
||||||
tag_history, revision_history, context)
|
|
||||||
rescue StandardError => error
|
|
||||||
# 解析できない版も捨てず,当時の Markdown を説明文として保存する.
|
|
||||||
say "#{ context }: 構造化を保留し,本文を保存します (#{ error.message })."
|
|
||||||
description = body
|
|
||||||
placements = []
|
|
||||||
members = []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
{ revision:, version_no: index + 1, description:, members:, placements:, body: }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# 自己包含は禁止されてゐるため,当該版は原文の保存を優先する.
|
|
||||||
prepared.each do |group_id, versions|
|
|
||||||
current = versions.last
|
|
||||||
next unless current.fetch(:members).any? { |member| member['tag_group_id'] == group_id }
|
|
||||||
|
|
||||||
say "Tag Group ##{ group_id }: 直接の自己参照を検出したため,現行構造化を保留します."
|
|
||||||
current[:description] = current.fetch(:body)
|
|
||||||
current[:members] = []
|
|
||||||
current[:placements] = []
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
validate_current_groups!(prepared)
|
|
||||||
rescue StandardError => error
|
|
||||||
say "Tag Group の包含関係に問題がありますが,移行を継続します (#{ error.message })."
|
|
||||||
end
|
|
||||||
|
|
||||||
# 移行先の書込みはすべて検証の後.DDL は含めない.
|
|
||||||
connection.transaction do
|
|
||||||
pages.each do |page|
|
|
||||||
page_id = page.fetch('id').to_i
|
|
||||||
current = prepared.fetch(page_id).last
|
|
||||||
|
|
||||||
insert_row('tag_groups', {
|
|
||||||
id: page_id,
|
|
||||||
# page.version_no ではなく,この Wiki に紐付く Revision の総数.
|
|
||||||
version_no: revisions_by_page.fetch(page_id).size,
|
|
||||||
name: page.fetch('name').delete_prefix(PREFIX),
|
|
||||||
description: current.fetch(:description),
|
|
||||||
created_by_user_id: page.fetch('created_user_id'),
|
|
||||||
updated_by_user_id: page.fetch('updated_user_id'),
|
|
||||||
created_at: page.fetch('created_at'),
|
|
||||||
updated_at: page.fetch('updated_at') })
|
|
||||||
end
|
|
||||||
|
|
||||||
pages.each do |page|
|
|
||||||
page_id = page.fetch('id').to_i
|
|
||||||
name = page.fetch('name').delete_prefix(PREFIX)
|
|
||||||
|
|
||||||
prepared.fetch(page_id).each do |item|
|
|
||||||
revision = item.fetch(:revision)
|
|
||||||
|
|
||||||
insert_row('tag_group_versions', {
|
|
||||||
id: revision.fetch('id'),
|
|
||||||
tag_group_id: page_id,
|
|
||||||
version_no: item.fetch(:version_no),
|
|
||||||
event_type: item.fetch(:version_no) == 1 ? 'create' : 'update',
|
|
||||||
name:,
|
|
||||||
description: item.fetch(:description),
|
|
||||||
members: JSON.generate(item.fetch(:members)),
|
|
||||||
placements: JSON.generate(item.fetch(:placements)),
|
|
||||||
created_by_user_id: revision.fetch('created_user_id'),
|
|
||||||
created_at: revision.fetch('created_at') })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
pages.each do |page|
|
|
||||||
page_id = page.fetch('id').to_i
|
|
||||||
current = prepared.fetch(page_id).last
|
|
||||||
revision = current.fetch(:revision)
|
|
||||||
|
|
||||||
current.fetch(:members).each do |member|
|
|
||||||
if member.key?('tag_id')
|
|
||||||
insert_row('tag_group_tags', {
|
|
||||||
tag_group_id: page_id,
|
|
||||||
tag_id: member.fetch('tag_id'),
|
|
||||||
created_by_user_id: revision.fetch('created_user_id'),
|
|
||||||
created_at: revision.fetch('created_at') })
|
|
||||||
else
|
|
||||||
insert_row('tag_group_groups', {
|
|
||||||
tag_group_id: page_id,
|
|
||||||
child_tag_group_id: member.fetch('tag_group_id'),
|
|
||||||
created_by_user_id: revision.fetch('created_user_id'),
|
|
||||||
created_at: revision.fetch('created_at') })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
pages.each do |page|
|
|
||||||
page_id = page.fetch('id').to_i
|
|
||||||
current = prepared.fetch(page_id).last
|
|
||||||
revision = current.fetch(:revision)
|
|
||||||
position = 0
|
|
||||||
|
|
||||||
append = -> nodes, parent_position do
|
|
||||||
nodes.each do |node|
|
|
||||||
position += 1
|
|
||||||
own_position = position
|
|
||||||
|
|
||||||
insert_row('tag_group_placements', {
|
|
||||||
tag_group_id: page_id,
|
|
||||||
position: own_position,
|
|
||||||
parent_position:,
|
|
||||||
section_name: node['section_name'],
|
|
||||||
description: node['description'],
|
|
||||||
tag_id: node['tag_id'],
|
|
||||||
child_tag_group_id: node['tag_group_id'],
|
|
||||||
created_by_user_id: revision.fetch('created_user_id'),
|
|
||||||
updated_by_user_id: revision.fetch('created_user_id'),
|
|
||||||
created_at: revision.fetch('created_at'),
|
|
||||||
updated_at: revision.fetch('created_at') })
|
|
||||||
|
|
||||||
append.(node.fetch('children'), own_position)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
append.(current.fetch(:placements), nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def assert_empty_destination!
|
|
||||||
['tag_groups', 'tag_group_versions', 'tag_group_tags',
|
|
||||||
'tag_group_groups', 'tag_group_placements'].each do |table|
|
|
||||||
count = connection.select_value("SELECT COUNT(*) FROM #{ table }").to_i
|
|
||||||
raise "#{ table } が空ではありません." if count.positive?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# 解決は tag_names.name -> tag_names.tag_id のみ.
|
|
||||||
# tags.tag_name_id は読まない.
|
|
||||||
def load_tag_ids
|
|
||||||
rows = connection.select_all(<<~SQL).to_a
|
|
||||||
SELECT
|
|
||||||
name
|
|
||||||
, tag_id
|
|
||||||
FROM
|
|
||||||
tag_names
|
|
||||||
WHERE
|
|
||||||
language_code = 'ja'
|
|
||||||
AND tag_id IS NOT NULL
|
|
||||||
SQL
|
|
||||||
|
|
||||||
rows.each_with_object({ }) do |row, by_name|
|
|
||||||
name = row.fetch('name')
|
|
||||||
id = row.fetch('tag_id').to_i
|
|
||||||
by_name[name] ||= []
|
|
||||||
by_name[name] << id unless by_name[name].include?(id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def revision_body revision_id
|
|
||||||
connection.select_rows(<<~SQL).flatten.join("\n")
|
|
||||||
SELECT
|
|
||||||
wl.body
|
|
||||||
FROM
|
|
||||||
wiki_revision_lines wrl
|
|
||||||
INNER JOIN wiki_lines wl ON wl.id = wrl.wiki_line_id
|
|
||||||
WHERE
|
|
||||||
wrl.wiki_revision_id = #{ Integer(revision_id) }
|
|
||||||
ORDER BY
|
|
||||||
wrl.position
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_wiki markdown, tag_ids, group_ids, context
|
|
||||||
root = Kramdown::Document.new(markdown, input: 'GFM').root
|
|
||||||
placements = []
|
|
||||||
description_parts = []
|
|
||||||
headings = []
|
|
||||||
|
|
||||||
root.children.each_with_index do |node, index|
|
|
||||||
case node.type
|
|
||||||
when :blank
|
|
||||||
next
|
|
||||||
when :header
|
|
||||||
level = node.options.fetch(:level)
|
|
||||||
name = inline_text(node).strip
|
|
||||||
raise "#{ context }: 名前のない見出しがあります." if name.empty?
|
|
||||||
|
|
||||||
headings.pop while headings.any? && headings.last.first >= level
|
|
||||||
parent = headings.last&.last
|
|
||||||
section = { 'section_name' => name, 'description' => nil, 'children' => [] }
|
|
||||||
(parent ? parent.fetch('children') : placements) << section
|
|
||||||
headings << [level, section]
|
|
||||||
when :p
|
|
||||||
text = inline_markdown(node).strip
|
|
||||||
next if text.empty?
|
|
||||||
|
|
||||||
if headings.any?
|
|
||||||
heading = headings.last.last
|
|
||||||
heading['description'] = [heading['description'], text].compact.join("\n\n")
|
|
||||||
else
|
|
||||||
description_parts << text
|
|
||||||
end
|
|
||||||
when :ul, :ol
|
|
||||||
if headings.any? && see_also_heading?(headings.last.last.fetch('section_name'))
|
|
||||||
heading = headings.last.last
|
|
||||||
raw = original_block(markdown, root.children, index)
|
|
||||||
heading['description'] = [heading['description'], raw].compact.join("\n\n")
|
|
||||||
else
|
|
||||||
target = headings.any? ? headings.last.last.fetch('children') : placements
|
|
||||||
target.concat(node.children.map { |item| parse_list_item(item, tag_ids, group_ids, context) })
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise "#{ context }: 未対応の Markdown 構文 #{ node.type.inspect }"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[description_parts.empty? ? nil : description_parts.join("\n\n"), placements]
|
|
||||||
end
|
|
||||||
|
|
||||||
def see_also_heading? name
|
|
||||||
['see also', '関連項目'].include?(name.strip.downcase.sub(/[::]\z/, ''))
|
|
||||||
end
|
|
||||||
|
|
||||||
def original_block markdown, siblings, index
|
|
||||||
current = siblings.fetch(index)
|
|
||||||
start_line = current.options[:location]
|
|
||||||
raise 'Markdown block の位置が不明です.' unless start_line
|
|
||||||
|
|
||||||
following = siblings[(index + 1)..].find do |node|
|
|
||||||
node.options[:location] && node.options[:location] > start_line
|
|
||||||
end
|
|
||||||
end_line = following ? following.options.fetch(:location) - 1 : markdown.lines.length
|
|
||||||
markdown.lines[(start_line - 1)...end_line].join.strip
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_list_item item, tag_ids, group_ids, context
|
|
||||||
raise "#{ context }: リスト項目ではありません." if item.type != :li
|
|
||||||
|
|
||||||
nested, own = item.children.partition { |child| [:ul, :ol].include?(child.type) }
|
|
||||||
own.reject! { |child| child.type == :blank }
|
|
||||||
unless own.all? { |child| [:p, :text, :a, :codespan].include?(child.type) }
|
|
||||||
raise "#{ context }: 未対応のリスト項目があります."
|
|
||||||
end
|
|
||||||
|
|
||||||
label = own.map { |child| inline_text(child) }.join(' ').strip
|
|
||||||
raise "#{ context }: 空のリスト項目があります." if label.empty?
|
|
||||||
|
|
||||||
node =
|
|
||||||
if group_ids.key?(label)
|
|
||||||
{ 'tag_group_id' => group_ids.fetch(label) }
|
|
||||||
elsif tag_ids.key?(label)
|
|
||||||
ids = tag_ids.fetch(label)
|
|
||||||
unless ids.one?
|
|
||||||
raise "#{ context }: タグ名 #{ label.inspect } が複数の ID を指します:#{ ids.inspect }"
|
|
||||||
end
|
|
||||||
{ 'tag_id' => ids.first }
|
|
||||||
elsif label.end_with?(':', ':')
|
|
||||||
name = label.sub(/[::]\z/, '')
|
|
||||||
raise "#{ context }: 空の区分名があります." if name.empty?
|
|
||||||
if see_also_heading?(name) && nested.any?
|
|
||||||
raise "#{ context }: リスト内の See also は説明文への保管処理が未実装です."
|
|
||||||
end
|
|
||||||
{ 'section_name' => name, 'description' => nil }
|
|
||||||
else
|
|
||||||
raise "#{ context }: 解決不能なリスト項目 #{ label.inspect }"
|
|
||||||
end
|
|
||||||
|
|
||||||
node['children'] = nested.flat_map do |list|
|
|
||||||
list.children.map { |child| parse_list_item(child, tag_ids, group_ids, context) }
|
|
||||||
end
|
|
||||||
|
|
||||||
node
|
|
||||||
end
|
|
||||||
|
|
||||||
def inline_text node
|
|
||||||
case node.type
|
|
||||||
when :text, :codespan
|
|
||||||
node.value.to_s
|
|
||||||
when :softbreak, :br
|
|
||||||
' '
|
|
||||||
when :a, :p, :header, :em, :strong, :span, :li
|
|
||||||
node.children.map { |child| inline_text(child) }.join
|
|
||||||
else
|
|
||||||
raise "未対応のインライン構文 #{ node.type.inspect }"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def inline_markdown node
|
|
||||||
case node.type
|
|
||||||
when :text
|
|
||||||
node.value.to_s
|
|
||||||
when :codespan
|
|
||||||
"`#{ node.value }`"
|
|
||||||
when :softbreak
|
|
||||||
"\n"
|
|
||||||
when :br
|
|
||||||
" \n"
|
|
||||||
when :p
|
|
||||||
node.children.map { |child| inline_markdown(child) }.join
|
|
||||||
when :a
|
|
||||||
href = node.attr.fetch('href')
|
|
||||||
"[#{ node.children.map { |child| inline_markdown(child) }.join }](#{ href })"
|
|
||||||
when :em
|
|
||||||
"*#{ node.children.map { |child| inline_markdown(child) }.join }*"
|
|
||||||
when :strong
|
|
||||||
"**#{ node.children.map { |child| inline_markdown(child) }.join }**"
|
|
||||||
else
|
|
||||||
raise "未対応のインライン構文 #{ node.type.inspect }"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_tree! nodes, context, depth = 1
|
|
||||||
raise "#{ context }: 配置の深さが 5 を超えてゐます." if depth > 5 && nodes.any?
|
|
||||||
|
|
||||||
nodes.each do |node|
|
|
||||||
name = node['section_name']
|
|
||||||
if name && name.length > 255
|
|
||||||
raise "#{ context }: 区分名が 255 文字を超えてゐます."
|
|
||||||
end
|
|
||||||
validate_tree!(node.fetch('children'), context, depth + 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_members nodes, date, tag_history, group_history, context
|
|
||||||
ids_of_tags = []
|
|
||||||
ids_of_groups = []
|
|
||||||
|
|
||||||
visit = -> items do
|
|
||||||
items.each do |node|
|
|
||||||
ids_of_tags << node['tag_id'] if node.key?('tag_id')
|
|
||||||
ids_of_groups << node['tag_group_id'] if node.key?('tag_group_id')
|
|
||||||
visit.(node.fetch('children'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
visit.(nodes)
|
|
||||||
|
|
||||||
tags = ids_of_tags.uniq.map do |id|
|
|
||||||
{ 'tag_id' => id,
|
|
||||||
'version_no' => version_at(tag_history[id], date, context, "Tag ##{ id }") }
|
|
||||||
end
|
|
||||||
|
|
||||||
groups = ids_of_groups.uniq.map do |id|
|
|
||||||
{ 'tag_group_id' => id,
|
|
||||||
'version_no' => version_at(group_history[id], date, context, "TagGroup ##{ id }") }
|
|
||||||
end
|
|
||||||
|
|
||||||
tags + groups
|
|
||||||
end
|
|
||||||
|
|
||||||
def version_at history, date, context, target
|
|
||||||
raise "#{ context }: #{ target } の履歴が存在しません." unless history&.any?
|
|
||||||
|
|
||||||
timestamp = Time.parse(date.to_s)
|
|
||||||
candidates = history.select do |row|
|
|
||||||
Time.parse(row.fetch('created_at').to_s) <= timestamp
|
|
||||||
end
|
|
||||||
if candidates.empty?
|
|
||||||
raise "#{ context }: #{ target } は #{ date } 時点で存在しません."
|
|
||||||
end
|
|
||||||
|
|
||||||
candidates.max_by { |row|
|
|
||||||
[Time.parse(row.fetch('created_at').to_s), row.fetch('version_no').to_i]
|
|
||||||
}.fetch('version_no').to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_current_groups! prepared
|
|
||||||
edges = { }
|
|
||||||
prepared.each do |group_id, versions|
|
|
||||||
edges[group_id] = versions.last.fetch(:members)
|
|
||||||
.filter_map { |member| member['tag_group_id'] }
|
|
||||||
end
|
|
||||||
|
|
||||||
visiting = { }
|
|
||||||
visited = { }
|
|
||||||
visit = -> group_id do
|
|
||||||
raise "Tag Group の循環参照を検出しました:#{ group_id }" if visiting[group_id]
|
|
||||||
return if visited[group_id]
|
|
||||||
|
|
||||||
visiting[group_id] = true
|
|
||||||
edges.fetch(group_id, []).each { |child_id| visit.(child_id) }
|
|
||||||
visiting.delete(group_id)
|
|
||||||
visited[group_id] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
edges.keys.each { |group_id| visit.(group_id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def quote(value) = connection.quote(value)
|
|
||||||
|
|
||||||
def insert_row table, values
|
|
||||||
columns = values.keys.join(', ')
|
|
||||||
payload = values.values.map { |value| quote(value) }.join(', ')
|
|
||||||
execute "INSERT INTO #{ table } (#{ columns }) VALUES (#{ payload })"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
生成ファイル
+18
-198
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
|
||||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -138,29 +138,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true
|
t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true
|
||||||
end
|
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.integer "region_code", limit: 2, null: false
|
|
||||||
t.string "variants", default: "", 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", "script_code", "region_code", "variants"], name: "idx_on_language_code_script_code_region_code_varian_b0ba85c39a", unique: true
|
|
||||||
t.index ["language_code"], name: "index_locales_on_language_code"
|
|
||||||
t.index ["region_code"], name: "fk_rails_175159992c"
|
|
||||||
t.index ["script_code"], name: "fk_rails_0b74ce96a8"
|
|
||||||
t.check_constraint "regexp_like(`variants`,_utf8mb4'^((([a-z0-9]{5,8}|[0-9][a-z0-9]{3})(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(-x(-[a-z0-9]{1,8})+)?)|(x(-[a-z0-9]{1,8})+))?$',_utf8mb4'c')", name: "locales_variants_check"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "material_id", null: false
|
t.bigint "material_id", null: false
|
||||||
t.string "profile", default: "legacy_drive", null: false
|
t.string "profile", default: "legacy_drive", null: false
|
||||||
@@ -411,24 +388,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
|
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "regions", primary_key: "code", id: { type: :integer, limit: 2 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.string "alpha_code", limit: 2
|
|
||||||
t.string "name", null: false
|
|
||||||
t.datetime "deprecated_at"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.index ["alpha_code"], name: "index_regions_on_alpha_code", unique: true
|
|
||||||
t.index ["deprecated_at"], name: "index_regions_on_deprecated_at"
|
|
||||||
t.check_constraint "regexp_like(`alpha_code`,_utf8mb4'^[A-Z]{2}$',_utf8mb4'c')", name: "regions_alpha_code_check"
|
|
||||||
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"
|
|
||||||
t.check_constraint "regexp_like(`code`,_utf8mb4'^[A-Z][a-z]{3}$',_utf8mb4'c')", name: "scripts_code_check"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
@@ -440,123 +399,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tag_description_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "tag_id", null: false
|
|
||||||
t.string "language_code", limit: 16, null: false
|
|
||||||
t.integer "version_no", null: false
|
|
||||||
t.string "event_type", null: false
|
|
||||||
t.text "body", null: false
|
|
||||||
t.string "source_language_code", limit: 16
|
|
||||||
t.integer "source_version_no"
|
|
||||||
t.bigint "created_by_user_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.index ["created_by_user_id"], name: "index_tag_description_versions_on_created_by_user_id"
|
|
||||||
t.index ["tag_id", "language_code", "version_no"], name: "idx_on_tag_id_language_code_version_no_ced77e57e6", unique: true
|
|
||||||
t.index ["tag_id", "source_language_code", "source_version_no"], name: "fk_rails_4f13094dbc"
|
|
||||||
t.index ["tag_id"], name: "index_tag_description_versions_on_tag_id"
|
|
||||||
t.check_constraint "((`source_language_code` is null) and (`source_version_no` is null)) or ((`source_language_code` is not null) and (`source_version_no` is not null))", name: "chk_tag_description_versions_source_pair"
|
|
||||||
t.check_constraint "(`source_language_code` is null) or (`source_language_code` <> `language_code`)", name: "chk_tag_description_versions_source_language_code_different"
|
|
||||||
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "tag_description_versions_event_type_valid"
|
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_tag_description_versions_version_no_positive"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_descriptions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "tag_id", null: false
|
|
||||||
t.string "language_code", limit: 16, null: false
|
|
||||||
t.integer "version_no", null: false
|
|
||||||
t.text "body", null: false
|
|
||||||
t.string "source_language_code", limit: 16
|
|
||||||
t.integer "source_version_no"
|
|
||||||
t.bigint "created_by_user_id"
|
|
||||||
t.bigint "updated_by_user_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["created_by_user_id"], name: "index_tag_descriptions_on_created_by_user_id"
|
|
||||||
t.index ["language_code"], name: "index_tag_descriptions_on_language_code"
|
|
||||||
t.index ["source_language_code", "source_version_no"], name: "idx_on_source_language_code_source_version_no_00117fe02a"
|
|
||||||
t.index ["tag_id", "language_code"], name: "index_tag_descriptions_on_tag_id_and_language_code", unique: true
|
|
||||||
t.index ["tag_id", "source_language_code", "source_version_no"], name: "fk_rails_d1be0a7696"
|
|
||||||
t.index ["tag_id"], name: "index_tag_descriptions_on_tag_id"
|
|
||||||
t.index ["updated_by_user_id"], name: "index_tag_descriptions_on_updated_by_user_id"
|
|
||||||
t.check_constraint "((`source_language_code` is null) and (`source_version_no` is null)) or ((`source_language_code` is not null) and (`source_version_no` is not null))", name: "chk_tag_descriptions_source_pair"
|
|
||||||
t.check_constraint "(`source_language_code` is null) or (`source_language_code` <> `language_code`)", name: "chk_tag_descriptions_source_language_code_different"
|
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_tag_descriptions_version_no_positive"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_group_groups", primary_key: ["tag_group_id", "child_tag_group_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "tag_group_id", null: false
|
|
||||||
t.bigint "child_tag_group_id", null: false
|
|
||||||
t.bigint "created_by_user_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.index ["child_tag_group_id"], name: "index_tag_group_groups_on_child_tag_group_id"
|
|
||||||
t.index ["created_by_user_id"], name: "index_tag_group_groups_on_created_by_user_id"
|
|
||||||
t.check_constraint "`tag_group_id` <> `child_tag_group_id`", name: "chk_tag_group_groups_tag_group_id_not_child_tag_group_id"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_group_placements", primary_key: ["tag_group_id", "position"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "tag_group_id", null: false
|
|
||||||
t.integer "position", null: false
|
|
||||||
t.integer "parent_position"
|
|
||||||
t.string "section_name"
|
|
||||||
t.text "description"
|
|
||||||
t.bigint "tag_id"
|
|
||||||
t.bigint "child_tag_group_id"
|
|
||||||
t.bigint "created_by_user_id"
|
|
||||||
t.bigint "updated_by_user_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["child_tag_group_id"], name: "index_tag_group_placements_on_child_tag_group_id"
|
|
||||||
t.index ["created_by_user_id"], name: "index_tag_group_placements_on_created_by_user_id"
|
|
||||||
t.index ["tag_group_id", "child_tag_group_id"], name: "fk_rails_7342b54ced"
|
|
||||||
t.index ["tag_group_id", "parent_position"], name: "fk_rails_3af6ee71ca"
|
|
||||||
t.index ["tag_group_id", "tag_id"], name: "fk_rails_365dc60759"
|
|
||||||
t.index ["tag_id"], name: "index_tag_group_placements_on_tag_id"
|
|
||||||
t.index ["updated_by_user_id"], name: "index_tag_group_placements_on_updated_by_user_id"
|
|
||||||
t.check_constraint "(((`section_name` is not null) and (`tag_id` is null) and (`child_tag_group_id` is null)) or ((`section_name` is null) and (`tag_id` is not null) and (`child_tag_group_id` is null)) or ((`section_name` is null) and (`tag_id` is null) and (`child_tag_group_id` is not null))) and ((`section_name` is not null) or (`description` is null))", name: "chk_tag_group_placements_references_exclusive"
|
|
||||||
t.check_constraint "`position` > `parent_position`", name: "chk_tag_group_placements_position_gt_parent_position"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_group_tags", primary_key: ["tag_group_id", "tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "tag_group_id", null: false
|
|
||||||
t.bigint "tag_id", null: false
|
|
||||||
t.bigint "created_by_user_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.index ["created_by_user_id"], name: "index_tag_group_tags_on_created_by_user_id"
|
|
||||||
t.index ["tag_id"], name: "index_tag_group_tags_on_tag_id"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_group_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "tag_group_id", null: false
|
|
||||||
t.integer "version_no", null: false
|
|
||||||
t.string "event_type", null: false
|
|
||||||
t.string "name", null: false
|
|
||||||
t.text "description"
|
|
||||||
t.json "members", null: false
|
|
||||||
t.json "placements", null: false
|
|
||||||
t.bigint "created_by_user_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.index ["created_by_user_id"], name: "index_tag_group_versions_on_created_by_user_id"
|
|
||||||
t.index ["tag_group_id", "version_no"], name: "index_tag_group_versions_on_tag_group_id_and_version_no", unique: true
|
|
||||||
t.index ["tag_group_id"], name: "index_tag_group_versions_on_tag_group_id"
|
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_tag_group_versions_version_no_positive"
|
|
||||||
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"children\"],\"properties\":{\"section_name\":{\"type\":\"string\"},\"description\":{\"type\":[\"string\",\"null\"]},\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"tag_group_id\":{\"type\":\"integer\",\"minimum\":1},\"children\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"children\"],\"properties\":{\"section_name\":{\"type\":\"string\"},\"description\":{\"type\":[\"string\",\"null\"]},\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"tag_group_id\":{\"type\":\"integer\",\"minimum\":1},\"children\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"children\"],\"properties\":{\"section_name\":{\"type\":\"string\"},\"description\":{\"type\":[\"string\",\"null\"]},\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"tag_group_id\":{\"type\":\"integer\",\"minimum\":1},\"children\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"children\"],\"properties\":{\"section_name\":{\"type\":\"string\"},\"description\":{\"type\":[\"string\",\"null\"]},\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"tag_group_id\":{\"type\":\"integer\",\"minimum\":1},\"children\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"children\"],\"properties\":{\"section_name\":{\"type\":\"string\"},\"description\":{\"type\":[\"string\",\"null\"]},\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"tag_group_id\":{\"type\":\"integer\",\"minimum\":1},\"children\":{\"type\":\"array\",\"maxItems\":0}},\"oneOf\":[{\"required\":[\"section_name\"]},{\"required\":[\"tag_id\"]},{\"required\":[\"tag_group_id\"]}],\"dependencies\":{\"description\":[\"section_name\"]},\"additionalProperties\":false}}},\"oneOf\":[{\"required\":[\"section_name\"]},{\"required\":[\"tag_id\"]},{\"required\":[\"tag_group_id\"]}],\"dependencies\":{\"description\":[\"section_name\"]},\"additionalProperties\":false}}},\"oneOf\":[{\"required\":[\"section_name\"]},{\"required\":[\"tag_id\"]},{\"required\":[\"tag_group_id\"]}],\"dependencies\":{\"description\":[\"section_name\"]},\"additionalProperties\":false}}},\"oneOf\":[{\"required\":[\"section_name\"]},{\"required\":[\"tag_id\"]},{\"required\":[\"tag_group_id\"]}],\"dependencies\":{\"description\":[\"section_name\"]},\"additionalProperties\":false}}},\"oneOf\":[{\"required\":[\"section_name\"]},{\"required\":[\"tag_id\"]},{\"required\":[\"tag_group_id\"]}],\"dependencies\":{\"description\":[\"section_name\"]},\"additionalProperties\":false}}',`placements`)", name: "chk_tag_group_versions_placements_schema"
|
|
||||||
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"uniqueItems\":true,\"items\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"tag_id\",\"version_no\"],\"properties\":{\"tag_id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1}},\"additionalProperties\":false},{\"type\":\"object\",\"required\":[\"tag_group_id\",\"version_no\"],\"properties\":{\"tag_group_id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1}},\"additionalProperties\":false}]}}',`members`)", name: "chk_tag_group_versions_members_schema"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_groups", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.integer "version_no", null: false
|
|
||||||
t.string "name", null: false
|
|
||||||
t.text "description"
|
|
||||||
t.bigint "created_by_user_id"
|
|
||||||
t.bigint "updated_by_user_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["created_by_user_id"], name: "index_tag_groups_on_created_by_user_id"
|
|
||||||
t.index ["name"], name: "index_tag_groups_on_name", unique: true
|
|
||||||
t.index ["updated_by_user_id"], name: "index_tag_groups_on_updated_by_user_id"
|
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_tag_groups_version_no_positive"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "parent_tag_id", null: false
|
t.bigint "parent_tag_id", null: false
|
||||||
@@ -578,22 +420,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
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 "name", null: false
|
||||||
t.string "script_code", limit: 4, null: false
|
|
||||||
t.boolean "primary_flg", null: false
|
|
||||||
t.boolean "auto_generated", default: false, null: false
|
|
||||||
t.bigint "canonical_id"
|
t.bigint "canonical_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.virtual "primary_tag_id", type: :bigint, as: "(case when `primary_flg` then `tag_id` else NULL end)"
|
|
||||||
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
|
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 ["name"], name: "index_tag_names_on_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.check_constraint "(0 <> `primary_flg`) or (0 = `auto_generated`)", name: "chk_tag_names_auto_generated_primary"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -615,13 +447,11 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
t.datetime "deprecated_at"
|
t.datetime "deprecated_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.bigint "created_by_user_id"
|
t.bigint "created_by_user_id"
|
||||||
t.json "names", null: false
|
|
||||||
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
||||||
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
|
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
|
||||||
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
|
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
|
||||||
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
|
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
|
||||||
t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive"
|
t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive"
|
||||||
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"object\",\"minProperties\":1,\"patternProperties\":{\"^.{1,16}$\":{\"type\":\"object\",\"required\":[\"name\",\"script_code\",\"auto_generated\",\"aliases\"],\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":255},\"script_code\":{\"type\":\"string\",\"pattern\":\"^[A-Z][a-z]{3}$\"},\"auto_generated\":{\"type\":\"boolean\"},\"aliases\":{\"type\":\"array\",\"uniqueItems\":true,\"items\":{\"type\":\"object\",\"required\":[\"name\",\"script_code\"],\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":255},\"script_code\":{\"type\":\"string\",\"pattern\":\"^[A-Z][a-z]{3}$\"}},\"additionalProperties\":false}}},\"additionalProperties\":false}},\"additionalProperties\":false}',`names`)", name: "chk_tag_versions_names_schema"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -773,6 +603,19 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
t.index ["banned_at"], name: "index_users_on_banned_at"
|
t.index ["banned_at"], name: "index_users_on_banned_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "wiki_assets", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.bigint "wiki_page_id", null: false
|
||||||
|
t.integer "no", null: false
|
||||||
|
t.string "alt_text"
|
||||||
|
t.binary "sha256", limit: 32, null: false
|
||||||
|
t.bigint "created_by_user_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["created_by_user_id"], name: "index_wiki_assets_on_created_by_user_id"
|
||||||
|
t.index ["wiki_page_id", "no"], name: "index_wiki_assets_on_wiki_page_id_and_no", unique: true
|
||||||
|
t.index ["wiki_page_id", "sha256"], name: "index_wiki_assets_on_wiki_page_id_and_sha256", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "sha256", limit: 64, null: false
|
t.string "sha256", limit: 64, null: false
|
||||||
t.text "body", null: false
|
t.text "body", null: false
|
||||||
@@ -789,6 +632,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.datetime "discarded_at"
|
t.datetime "discarded_at"
|
||||||
|
t.integer "next_asset_no", default: 1, null: false
|
||||||
t.integer "version_no", null: false
|
t.integer "version_no", null: false
|
||||||
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
||||||
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
||||||
@@ -857,9 +701,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
add_foreign_key "gekanator_question_suggestions", "users"
|
add_foreign_key "gekanator_question_suggestions", "users"
|
||||||
add_foreign_key "gekanator_questions", "gekanator_question_suggestions"
|
add_foreign_key "gekanator_questions", "gekanator_question_suggestions"
|
||||||
add_foreign_key "gekanator_questions", "users", column: "created_by_id"
|
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", "regions", column: "region_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", "materials"
|
||||||
add_foreign_key "material_export_items", "users", column: "created_by_user_id"
|
add_foreign_key "material_export_items", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "material_import_blocks", "users", column: "created_by_user_id"
|
add_foreign_key "material_import_blocks", "users", column: "created_by_user_id"
|
||||||
@@ -891,32 +732,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
add_foreign_key "post_versions", "users", column: "created_by_user_id"
|
add_foreign_key "post_versions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||||
add_foreign_key "settings", "users"
|
add_foreign_key "settings", "users"
|
||||||
add_foreign_key "tag_description_versions", "tag_description_versions", column: ["tag_id", "source_language_code", "source_version_no"], primary_key: ["tag_id", "language_code", "version_no"]
|
|
||||||
add_foreign_key "tag_descriptions", "languages", column: "language_code", primary_key: "code"
|
|
||||||
add_foreign_key "tag_descriptions", "tag_description_versions", column: ["tag_id", "source_language_code", "source_version_no"], primary_key: ["tag_id", "language_code", "version_no"]
|
|
||||||
add_foreign_key "tag_descriptions", "tags"
|
|
||||||
add_foreign_key "tag_descriptions", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "tag_descriptions", "users", column: "updated_by_user_id"
|
|
||||||
add_foreign_key "tag_group_groups", "tag_groups"
|
|
||||||
add_foreign_key "tag_group_groups", "tag_groups", column: "child_tag_group_id"
|
|
||||||
add_foreign_key "tag_group_groups", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "tag_group_placements", "tag_group_groups", column: ["tag_group_id", "child_tag_group_id"], primary_key: ["tag_group_id", "child_tag_group_id"]
|
|
||||||
add_foreign_key "tag_group_placements", "tag_group_placements", column: ["tag_group_id", "parent_position"], primary_key: ["tag_group_id", "position"]
|
|
||||||
add_foreign_key "tag_group_placements", "tag_group_tags", column: ["tag_group_id", "tag_id"], primary_key: ["tag_group_id", "tag_id"]
|
|
||||||
add_foreign_key "tag_group_placements", "tag_groups"
|
|
||||||
add_foreign_key "tag_group_placements", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "tag_group_placements", "users", column: "updated_by_user_id"
|
|
||||||
add_foreign_key "tag_group_tags", "tag_groups"
|
|
||||||
add_foreign_key "tag_group_tags", "tags"
|
|
||||||
add_foreign_key "tag_group_tags", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "tag_groups", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "tag_groups", "users", column: "updated_by_user_id"
|
|
||||||
add_foreign_key "tag_implications", "tags"
|
add_foreign_key "tag_implications", "tags"
|
||||||
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
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", "tag_names", column: "canonical_id"
|
||||||
add_foreign_key "tag_names", "tags"
|
|
||||||
add_foreign_key "tag_similarities", "tags"
|
add_foreign_key "tag_similarities", "tags"
|
||||||
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
||||||
add_foreign_key "tag_versions", "users", column: "created_by_user_id"
|
add_foreign_key "tag_versions", "users", column: "created_by_user_id"
|
||||||
@@ -945,6 +763,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_08_010000) do
|
|||||||
add_foreign_key "user_post_views", "posts"
|
add_foreign_key "user_post_views", "posts"
|
||||||
add_foreign_key "user_post_views", "users"
|
add_foreign_key "user_post_views", "users"
|
||||||
add_foreign_key "user_theme_slots", "users"
|
add_foreign_key "user_theme_slots", "users"
|
||||||
|
add_foreign_key "wiki_assets", "users", column: "created_by_user_id"
|
||||||
|
add_foreign_key "wiki_assets", "wiki_pages"
|
||||||
add_foreign_key "wiki_pages", "tag_names"
|
add_foreign_key "wiki_pages", "tag_names"
|
||||||
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
||||||
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ namespace :nico do
|
|||||||
task export: :environment do
|
task export: :environment do
|
||||||
require 'open3'
|
require 'open3'
|
||||||
|
|
||||||
mysql_user = ENV.fetch('NIZIKA_NICO_MYSQL_USER')
|
mysql_user = ENV.fetch('MYSQL_USER')
|
||||||
mysql_pass = ENV.fetch('NIZIKA_NICO_MYSQL_PASS')
|
mysql_pass = ENV.fetch('MYSQL_PASS')
|
||||||
nizika_nico_path = ENV.fetch('NIZIKA_NICO_PATH')
|
nizika_nico_path = ENV.fetch('NIZIKA_NICO_PATH')
|
||||||
|
|
||||||
videos = Post.where('url LIKE ?', '%nicovideo.jp/watch/%').pluck(:url).filter_map {
|
videos = Post.where('url LIKE ?', '%nicovideo.jp/watch/%').pluck(:url).filter_map {
|
||||||
@@ -14,7 +14,7 @@ namespace :nico do
|
|||||||
next if videos.empty?
|
next if videos.empty?
|
||||||
|
|
||||||
_, stderr, status = Open3.capture3(
|
_, stderr, status = Open3.capture3(
|
||||||
{ 'NIZIKA_NICO_MYSQL_USER' => mysql_user, 'NIZIKA_NICO_MYSQL_PASS' => mysql_pass },
|
{ 'MYSQL_USER' => mysql_user, 'MYSQL_PASS' => mysql_pass },
|
||||||
'python3', '-m', 'tracked_videos.put_bulk_upsert', *videos,
|
'python3', '-m', 'tracked_videos.put_bulk_upsert', *videos,
|
||||||
chdir: nizika_nico_path)
|
chdir: nizika_nico_path)
|
||||||
|
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ namespace :nico do
|
|||||||
.find_each(&:destroy!)
|
.find_each(&:destroy!)
|
||||||
end
|
end
|
||||||
|
|
||||||
mysql_user = ENV['NIZIKA_NICO_MYSQL_USER']
|
mysql_user = ENV['MYSQL_USER']
|
||||||
mysql_pass = ENV['NIZIKA_NICO_MYSQL_PASS']
|
mysql_pass = ENV['MYSQL_PASS']
|
||||||
nizika_nico_path = ENV['NIZIKA_NICO_PATH']
|
nizika_nico_path = ENV['NIZIKA_NICO_PATH']
|
||||||
stdout, stderr, status = Open3.capture3(
|
stdout, stderr, status = Open3.capture3(
|
||||||
{ 'NIZIKA_NICO_MYSQL_USER' => mysql_user, 'NIZIKA_NICO_MYSQL_PASS' => mysql_pass },
|
{ 'MYSQL_USER' => mysql_user, 'MYSQL_PASS' => mysql_pass },
|
||||||
'python3', "#{ nizika_nico_path }/get_videos.py")
|
'python3', "#{ nizika_nico_path }/get_videos.py")
|
||||||
|
|
||||||
unless status.success?
|
unless status.success?
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ RSpec.describe 'discarded tag cleanup migrations' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with legacy records' do
|
context 'with legacy records' do
|
||||||
# Reproduce the historical schema required by these pre-multilingual migrations.
|
|
||||||
self.use_transactional_tests = false
|
self.use_transactional_tests = false
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
require_relative '../../db/migrate/20261003010000_migrate_tag_descriptions_from_wiki_pages'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag description migration identity' do
|
|
||||||
it 'preserves every revision ID and repeated body in an independent sequence' do
|
|
||||||
user = create_member_user!
|
|
||||||
tag_name = create(:tag_name, name: 'description_migration')
|
|
||||||
page = Wiki::Commit.create_content!(
|
|
||||||
tag_name:, body: 'A', created_by_user: user)
|
|
||||||
revisions = [page.current_revision]
|
|
||||||
['A', 'B', 'B', 'A'].each do |body|
|
|
||||||
revisions << Wiki::Commit.content!(
|
|
||||||
page:, body:, created_user: user, base_revision_id: revisions.last.id)
|
|
||||||
end
|
|
||||||
tag = create(:tag, primary_tag_name: tag_name)
|
|
||||||
legacy_state = [page.reload.attributes, revisions.map(&:attributes)]
|
|
||||||
expect(page.wiki_versions.count).to eq(3)
|
|
||||||
|
|
||||||
migration = MigrateTagDescriptionsFromWikiPages.new
|
|
||||||
migration.suppress_messages { migration.up }
|
|
||||||
|
|
||||||
versions = TagDescriptionVersion.where(tag_id: tag.id, language_code: 'ja')
|
|
||||||
.order(:version_no)
|
|
||||||
expect(versions.pluck(:id)).to eq(revisions.map(&:id))
|
|
||||||
expect(versions.pluck(:version_no)).to eq([1, 2, 3, 4, 5])
|
|
||||||
expect(versions.pluck(:body)).to eq(['A', 'A', 'B', 'B', 'A'])
|
|
||||||
expect(versions.pluck(:created_by_user_id)).to eq([user.id] * 5)
|
|
||||||
expect(versions.pluck(:created_at)).to eq(revisions.map(&:created_at))
|
|
||||||
expect(TagDescription.find(page.id)).to have_attributes(
|
|
||||||
tag_id: tag.id, language_code: 'ja', version_no: 5, body: 'A')
|
|
||||||
expect([page.reload.attributes, revisions.map { |revision| revision.reload.attributes }])
|
|
||||||
.to eq(legacy_state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag group persistence constraints' do
|
|
||||||
include TagGroupRecords
|
|
||||||
|
|
||||||
let!(:user) { create_member_user! }
|
|
||||||
let!(:tag) { group_tag('制約確認') }
|
|
||||||
let!(:group_id) do
|
|
||||||
seed_group(
|
|
||||||
name: '制約確認',
|
|
||||||
members: [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }])
|
|
||||||
end
|
|
||||||
|
|
||||||
def placement attributes
|
|
||||||
insert_group_row('tag_group_placements', {
|
|
||||||
tag_group_id: group_id, position: 1,
|
|
||||||
created_at: Time.current, updated_at: Time.current }.merge(attributes))
|
|
||||||
end
|
|
||||||
|
|
||||||
def reject_statement
|
|
||||||
before_state = group_state
|
|
||||||
expect { yield }.to raise_error(ActiveRecord::StatementInvalid)
|
|
||||||
expect(group_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows membership without placement' do
|
|
||||||
expect(group_rows('tag_group_tags', tag_group_id: group_id).size).to eq(1)
|
|
||||||
expect(group_rows('tag_group_placements', tag_group_id: group_id)).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows repeated Tag placements with unique membership' do
|
|
||||||
placement(tag_id: tag.id)
|
|
||||||
placement(tag_id: tag.id, position: 2)
|
|
||||||
expect(group_rows('tag_group_tags', tag_group_id: group_id).size).to eq(1)
|
|
||||||
expect(group_rows('tag_group_placements', tag_group_id: group_id).size).to eq(2)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects duplicate Tag membership' do
|
|
||||||
reject_statement do
|
|
||||||
insert_group_row('tag_group_tags', {
|
|
||||||
tag_group_id: group_id, tag_id: tag.id, created_at: Time.current })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects duplicate child membership' do
|
|
||||||
child_id = seed_group(name: '子')
|
|
||||||
attributes = { tag_group_id: group_id, child_tag_group_id: child_id,
|
|
||||||
created_at: Time.current }
|
|
||||||
insert_group_row('tag_group_groups', attributes)
|
|
||||||
reject_statement { insert_group_row('tag_group_groups', attributes) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects direct self membership' do
|
|
||||||
reject_statement do
|
|
||||||
insert_group_row('tag_group_groups', {
|
|
||||||
tag_group_id: group_id, child_tag_group_id: group_id, created_at: Time.current })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a placement without a kind' do
|
|
||||||
reject_statement { placement({ }) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects simultaneous Section and Tag kinds' do
|
|
||||||
reject_statement { placement(section_name: '区分', tag_id: tag.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a description attached to a Tag placement' do
|
|
||||||
reject_statement { placement(tag_id: tag.id, description: '不正') }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows an optional description on a Section' do
|
|
||||||
placement(section_name: '区分', description: '説明')
|
|
||||||
expect(group_rows('tag_group_placements', tag_group_id: group_id).sole)
|
|
||||||
.to include('section_name' => '区分', 'description' => '説明')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a Tag placement absent from membership' do
|
|
||||||
other = group_tag('非メンバー')
|
|
||||||
reject_statement { placement(tag_id: other.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a missing parent placement' do
|
|
||||||
reject_statement { placement(section_name: '子', parent_position: 9) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a child ordered before its parent' do
|
|
||||||
placement(section_name: '親', position: 2)
|
|
||||||
reject_statement { placement(section_name: '子', position: 1, parent_position: 2) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects duplicate group names' do
|
|
||||||
reject_statement { seed_group(name: '制約確認') }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a missing mandatory group name' do
|
|
||||||
reject_statement { seed_group(name: nil) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a six-level snapshot' do
|
|
||||||
nodes = []
|
|
||||||
6.times { nodes = [{ 'tag_id' => tag.id, 'children' => nodes }] }
|
|
||||||
reject_statement do
|
|
||||||
insert_group_row('tag_group_versions', {
|
|
||||||
tag_group_id: group_id, version_no: 2, event_type: 'update', name: '制約確認',
|
|
||||||
members: [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }].to_json,
|
|
||||||
placements: nodes.to_json, created_at: Time.current })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag group migration identity and structure' do
|
|
||||||
include TagGroupRecords
|
|
||||||
|
|
||||||
let!(:user) { create_member_user! }
|
|
||||||
|
|
||||||
before { I18n.locale = :ja }
|
|
||||||
|
|
||||||
it 'preserves page and revision IDs, timestamps and repeated historical structures' do
|
|
||||||
tag = group_tag('洗澡鹿')
|
|
||||||
body = "説明文\n\n- 洗澡鹿\n- 洗澡鹿"
|
|
||||||
page = legacy_group('伊地知ニジカ亜種', body)
|
|
||||||
revisions = [page.current_revision]
|
|
||||||
[body, '# 空の区分', body].each do |text|
|
|
||||||
revisions << Wiki::Commit.legacy_content!(page:, body: text, created_user: user)
|
|
||||||
end
|
|
||||||
before_state = group_state.except(*TagGroupRecords::GROUP_TABLES)
|
|
||||||
|
|
||||||
migrate_groups
|
|
||||||
|
|
||||||
versions = group_rows('tag_group_versions', tag_group_id: page.id)
|
|
||||||
.sort_by { _1.fetch('version_no') }
|
|
||||||
expect(versions.map { _1.fetch('id') }).to eq(revisions.map(&:id))
|
|
||||||
expect(versions.map { _1.fetch('version_no') }).to eq([1, 2, 3, 4])
|
|
||||||
expect(versions.map { _1.fetch('created_at') }).to eq(revisions.map(&:created_at))
|
|
||||||
expect(versions.map { _1.fetch('created_by_user_id') }).to eq([user.id] * 4)
|
|
||||||
expect(versions.map { _1.fetch('event_type') }).to eq(['create', 'update', 'update', 'update'])
|
|
||||||
expect(group_version(page.id, 1).fetch('placements')).to eq(
|
|
||||||
group_version(page.id, 2).fetch('placements'))
|
|
||||||
expect(group_version(page.id, 3).fetch('placements')).to eq([
|
|
||||||
{ 'section_name' => '空の区分', 'description' => nil, 'children' => [] }])
|
|
||||||
expect_group_snapshot(
|
|
||||||
page.id, number: 4, description: '説明文',
|
|
||||||
members: [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }],
|
|
||||||
placements: [{ 'tag_id' => tag.id, 'children' => [] }] * 2)
|
|
||||||
expect(group_rows('tag_groups', id: page.id).sole).to include(
|
|
||||||
'created_at' => page.created_at, 'updated_at' => page.reload.updated_at)
|
|
||||||
expect(group_state.except(*TagGroupRecords::GROUP_TABLES)).to eq(before_state)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'migrates nested list Sections and child groups as different placement kinds' do
|
|
||||||
series = group_tag('ぼざろクリーチャーシリーズ')
|
|
||||||
deer = group_tag('洗澡鹿')
|
|
||||||
nijika = group_tag('伊地知ニジカ')
|
|
||||||
child = legacy_group('伊地知ニジカ亜種', <<~MARKDOWN)
|
|
||||||
- ぼざろクリーチャーシリーズ
|
|
||||||
- 伊地知ニジカ亜種:
|
|
||||||
- 洗澡鹿
|
|
||||||
MARKDOWN
|
|
||||||
parent = legacy_group('ぼざろクリーチャーシリーズのキャラクター', <<~MARKDOWN)
|
|
||||||
- ぼざろクリーチャーシリーズ
|
|
||||||
- 伊地知ニジカ
|
|
||||||
- タググループ:伊地知ニジカ亜種
|
|
||||||
MARKDOWN
|
|
||||||
|
|
||||||
migrate_groups
|
|
||||||
|
|
||||||
expect_group_snapshot(
|
|
||||||
child.id, number: 1,
|
|
||||||
members: [series, deer].map { { 'tag_id' => _1.id, 'version_no' => _1.version_no } },
|
|
||||||
placements: [{ 'tag_id' => series.id, 'children' => [
|
|
||||||
{ 'section_name' => '伊地知ニジカ亜種', 'description' => nil,
|
|
||||||
'children' => [{ 'tag_id' => deer.id, 'children' => [] }] }] }])
|
|
||||||
expect_group_snapshot(
|
|
||||||
parent.id, number: 1,
|
|
||||||
members: [
|
|
||||||
{ 'tag_id' => series.id, 'version_no' => series.version_no },
|
|
||||||
{ 'tag_id' => nijika.id, 'version_no' => nijika.version_no },
|
|
||||||
{ 'tag_group_id' => child.id, 'version_no' => 1 }],
|
|
||||||
placements: [{ 'tag_id' => series.id, 'children' => [
|
|
||||||
{ 'tag_id' => nijika.id, 'children' => [
|
|
||||||
{ 'tag_group_id' => child.id, 'children' => [] }] }] }])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'preserves heading order, group paragraphs and Section descriptions' do
|
|
||||||
names = ['タグ希望', 'キャラクター名希望', '楽曲情報希望', 'bot操作', 'ニジラー情報不詳']
|
|
||||||
tags = names.map { group_tag(_1) }
|
|
||||||
markdown = <<~MARKDOWN
|
|
||||||
全体の説明
|
|
||||||
|
|
||||||
# 要求
|
|
||||||
要求の説明
|
|
||||||
|
|
||||||
- タグ希望
|
|
||||||
- キャラクター名希望
|
|
||||||
- 楽曲情報希望
|
|
||||||
|
|
||||||
# 情報
|
|
||||||
- bot操作
|
|
||||||
- ニジラー情報不詳
|
|
||||||
MARKDOWN
|
|
||||||
page = legacy_group('メタタグ', markdown)
|
|
||||||
expect(page.current_revision.body).to eq(markdown.chomp)
|
|
||||||
|
|
||||||
migrate_groups
|
|
||||||
|
|
||||||
leaves = tags.map { { 'tag_id' => _1.id, 'children' => [] } }
|
|
||||||
leaves.first['children'] = leaves[1..2]
|
|
||||||
expect_group_snapshot(
|
|
||||||
page.id, number: 1, description: '全体の説明',
|
|
||||||
members: tags.map { { 'tag_id' => _1.id, 'version_no' => _1.version_no } },
|
|
||||||
placements: [
|
|
||||||
{ 'section_name' => '要求', 'description' => '要求の説明',
|
|
||||||
'children' => [leaves.first] },
|
|
||||||
{ 'section_name' => '情報', 'description' => nil,
|
|
||||||
'children' => leaves[3..4] }])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'leaves Help in the legacy domain' do
|
|
||||||
page = Wiki::Commit.create_legacy_content!(
|
|
||||||
tag_name: create(:tag_name, name: 'Help:移行対象外'),
|
|
||||||
body: 'Help 本文', created_by_user: user)
|
|
||||||
before_state = group_state
|
|
||||||
migrate_groups
|
|
||||||
expect(group_state).to eq(before_state)
|
|
||||||
expect(page.reload.body).to eq('Help 本文')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'TagName database invariants' do
|
|
||||||
def insert_name!(**attributes)
|
|
||||||
TagName.insert_all!([{ name: 'constraint_name', language_code: 'ja',
|
|
||||||
script_code: 'Jpan', primary_flg: true,
|
|
||||||
created_at: Time.current, updated_at: Time.current,
|
|
||||||
**attributes }])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'defaults persisted names to human-confirmed provenance' do
|
|
||||||
insert_name!
|
|
||||||
|
|
||||||
expect(TagName.find_by!(name: 'constraint_name').auto_generated).to be(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
[[true, true], [true, false], [false, false]].each do |primary_flg, auto_generated|
|
|
||||||
it "allows primary=#{ primary_flg }, auto_generated=#{ auto_generated }" do
|
|
||||||
expect { insert_name!(primary_flg:, auto_generated:) }
|
|
||||||
.to change(TagName, :count).by(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects auto-generated aliases at the database boundary' do
|
|
||||||
expect { insert_name!(primary_flg: false, auto_generated: true) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a second primary for the same tag and language' do
|
|
||||||
tag = create(:tag)
|
|
||||||
|
|
||||||
expect { insert_name!(tag_id: tag.id, auto_generated: false) }
|
|
||||||
.to raise_error(ActiveRecord::RecordNotUnique)
|
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'TagVersion names database constraint' do
|
|
||||||
let(:tag) { create(:tag) }
|
|
||||||
let(:names) do
|
|
||||||
{ 'ja' => { 'name' => 'snapshot_name', 'script_code' => 'Jpan',
|
|
||||||
'auto_generated' => false, 'aliases' => [] } }
|
|
||||||
end
|
|
||||||
let(:alias_name) { { 'name' => 'snapshot_alias', 'script_code' => 'Jpan' } }
|
|
||||||
|
|
||||||
def insert_snapshot!(snapshot)
|
|
||||||
TagVersion.insert_all!([{ tag_id: tag.id, version_no: 1, event_type: 'create',
|
|
||||||
name: 'snapshot_name', category: 'general',
|
|
||||||
aliases: '', parent_tag_ids: '', names: snapshot,
|
|
||||||
created_at: Time.current }])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'accepts grouped Nipponese names with an empty alias list' do
|
|
||||||
expect { insert_snapshot!(names) }.to change(TagVersion, :count).by(1)
|
|
||||||
expect(TagVersion.find_by!(tag_id: tag.id).names).to eq(names)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'accepts aliases containing only their name and script' do
|
|
||||||
names['ja']['aliases'] = [alias_name]
|
|
||||||
|
|
||||||
expect { insert_snapshot!(names) }.to change(TagVersion, :count).by(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a flat array root' do
|
|
||||||
expect { insert_snapshot!([names.fetch('ja')]) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects an empty object root' do
|
|
||||||
expect { insert_snapshot!({ }) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a language entry that is not an object' do
|
|
||||||
expect { insert_snapshot!({ 'ja' => 'not an object' }) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
['name', 'script_code', 'auto_generated', 'aliases'].each do |field|
|
|
||||||
it "rejects a primary entry missing #{ field }" do
|
|
||||||
names['ja'].delete(field)
|
|
||||||
|
|
||||||
expect { insert_snapshot!(names) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects an empty primary name' do
|
|
||||||
names['ja']['name'] = ''
|
|
||||||
|
|
||||||
expect { insert_snapshot!(names) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects incorrect script casing' do
|
|
||||||
names['ja']['script_code'] = 'JPAN'
|
|
||||||
|
|
||||||
expect { insert_snapshot!(names) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
['unknown', 'primary_flg', 'language_code'].each do |field|
|
|
||||||
it "rejects the extra primary field #{ field }" do
|
|
||||||
names['ja'][field] = true
|
|
||||||
|
|
||||||
expect { insert_snapshot!(names) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
['unknown', 'primary_flg', 'auto_generated'].each do |field|
|
|
||||||
it "rejects the extra alias field #{ field }" do
|
|
||||||
names['ja']['aliases'] = [alias_name.merge(field => true)]
|
|
||||||
|
|
||||||
expect { insert_snapshot!(names) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects duplicate alias objects' do
|
|
||||||
names['ja']['aliases'] = [alias_name, alias_name.deep_dup]
|
|
||||||
|
|
||||||
expect { insert_snapshot!(names) }
|
|
||||||
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :tag_name do
|
factory :tag_name do
|
||||||
language_code { 'ja' }
|
name { "tag-#{SecureRandom.hex(4)}" }
|
||||||
script_code { 'Jpan' }
|
|
||||||
primary_flg { true }
|
|
||||||
auto_generated { false }
|
|
||||||
sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" }
|
|
||||||
|
|
||||||
trait :alias do
|
|
||||||
primary_flg { false }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,41 +1,15 @@
|
|||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :tag do
|
factory :tag do
|
||||||
transient do
|
transient do
|
||||||
primary_name { nil }
|
name { nil }
|
||||||
primary_tag_name do
|
|
||||||
attributes = primary_name.nil? ? { } : { name: primary_name }
|
|
||||||
build(:tag_name, **attributes)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
category { :general }
|
category { :general }
|
||||||
post_count { 0 }
|
post_count { 0 }
|
||||||
|
association :tag_name
|
||||||
|
|
||||||
after(:build) do |tag, evaluator|
|
after(:build) do |tag, evaluator|
|
||||||
evaluator.primary_tag_name.primary_flg = true
|
tag.name = evaluator.name if evaluator.name.present?
|
||||||
tag.tag_names.target << evaluator.primary_tag_name
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:create) do |tag, evaluator|
|
|
||||||
evaluator.primary_tag_name.update!(tag:, primary_flg: true)
|
|
||||||
|
|
||||||
Locale.where(deprecated_at: nil).order(:language_code, :created_at, :script_code).to_a
|
|
||||||
.uniq(&:language_code).each do |locale|
|
|
||||||
next if tag.tag_names.exists?(language_code: locale.language_code,
|
|
||||||
primary_flg: true)
|
|
||||||
|
|
||||||
create(:tag_name,
|
|
||||||
tag:, language_code: locale.language_code,
|
|
||||||
script_code: locale.script_code,
|
|
||||||
name: TagName.generate_name(locale, tag, evaluator.primary_tag_name),
|
|
||||||
primary_flg: true, auto_generated: true)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Language, type: :model do
|
|
||||||
it 'accepts Nipponese as the current supported language' do
|
|
||||||
expect(described_class.find('ja')).to be_valid
|
|
||||||
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['ja'])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'accepts English when multilingual support is enabled' do
|
|
||||||
pending '日本語以外の Language 有効化は別課題で実装する'
|
|
||||||
expect { described_class.create!(code: 'en', name: 'English') }
|
|
||||||
.to change(described_class, :count).by(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects an unsupported language code' do
|
|
||||||
expect {
|
|
||||||
described_class.create!(code: 'xx', name: 'Unsupported')
|
|
||||||
}.to raise_error { |error|
|
|
||||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
['en', 'fr'].each do |code|
|
|
||||||
it "does not register #{ code } as a currently supported language" do
|
|
||||||
expect {
|
|
||||||
described_class.create!(code:, name: 'Unsupported')
|
|
||||||
}.to raise_error { |error|
|
|
||||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Locale, type: :model do
|
|
||||||
include_context 'English locale'
|
|
||||||
|
|
||||||
def create_english_locale!
|
|
||||||
prepare_english_reference!
|
|
||||||
described_class.create!(code: 'en', language_code: 'en',
|
|
||||||
script_code: 'Latn', region_code: 826,
|
|
||||||
variants: '', name: 'English')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'keeps the active Nipponese locale consistent with its identity tuple' do
|
|
||||||
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['ja'])
|
|
||||||
expect(described_class.nipponese).to have_attributes(
|
|
||||||
code: 'ja', language_code: 'ja', script_code: 'Jpan',
|
|
||||||
region_code: 392, variants: '')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a duplicate locale identity tuple at the database boundary' do
|
|
||||||
expect {
|
|
||||||
described_class.insert_all!([{ code: 'ja-duplicate', language_code: 'ja',
|
|
||||||
script_code: 'Jpan', region_code: 392,
|
|
||||||
variants: '', name: '日本語',
|
|
||||||
created_at: Time.current }])
|
|
||||||
}.to raise_error(ActiveRecord::RecordNotUnique)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'generates a primary name for every existing tag using the name generator' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
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'))
|
|
||||||
.and_return('name_from_generator')
|
|
||||||
|
|
||||||
locale = create_english_locale!
|
|
||||||
|
|
||||||
expect(TagName).to have_received(:generate_name)
|
|
||||||
.with(locale, first, primary_tag_name_for(first, 'ja'))
|
|
||||||
expect(TagName).to have_received(:generate_name)
|
|
||||||
.with(locale, second, primary_tag_name_for(second, 'ja'))
|
|
||||||
expect(first.tag_names.find_by!(language_code: 'en', primary_flg: true))
|
|
||||||
.to have_attributes(name: 'name_from_generator', tag_id: first.id,
|
|
||||||
language_code: 'en', script_code: 'Latn',
|
|
||||||
primary_flg: true, auto_generated: true)
|
|
||||||
expect(second.tag_names.find_by!(language_code: 'en', primary_flg: true))
|
|
||||||
.to have_attributes(tag_id: second.id, language_code: 'en',
|
|
||||||
script_code: 'Latn', primary_flg: true,
|
|
||||||
auto_generated: true)
|
|
||||||
expect(TagName.where(language_code: 'en', 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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
prepare_english_reference!
|
|
||||||
tag = create(:tag, primary_name: 'already_named')
|
|
||||||
existing = create(:tag_name, name: 'existing_english', tag:,
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
|
|
||||||
create_english_locale!
|
|
||||||
|
|
||||||
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true))
|
|
||||||
.to contain_exactly(existing)
|
|
||||||
expect(existing.reload).to have_attributes(
|
|
||||||
tag_id: tag.id, language_code: 'en', script_code: 'Latn',
|
|
||||||
primary_flg: true, auto_generated: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a primary name when the language has only an alias' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
prepare_english_reference!
|
|
||||||
tag = create(:tag, primary_name: 'alias_only_tag')
|
|
||||||
alias_name = create(:tag_name, :alias, name: 'alias_en',
|
|
||||||
tag:,
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
|
|
||||||
create_english_locale!
|
|
||||||
|
|
||||||
expect(alias_name.reload).to have_attributes(
|
|
||||||
tag_id: tag.id,
|
|
||||||
language_code: 'en', script_code: 'Latn', primary_flg: false)
|
|
||||||
generated = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
|
|
||||||
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'en',
|
|
||||||
script_code: 'Latn', primary_flg: true,
|
|
||||||
auto_generated: true)
|
|
||||||
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true).count).to eq(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rolls back the locale and all generated names when backfill fails' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
prepare_english_reference!
|
|
||||||
first = create(:tag, primary_name: 'atomic_first')
|
|
||||||
second = create(:tag, primary_name: 'atomic_second')
|
|
||||||
allow(TagName).to receive(:generate_name).and_call_original
|
|
||||||
allow(TagName).to receive(:generate_name)
|
|
||||||
.with(kind_of(described_class), second, primary_tag_name_for(second, 'ja'))
|
|
||||||
.and_raise('backfill failed')
|
|
||||||
|
|
||||||
expect { create_english_locale! }.to raise_error('backfill failed')
|
|
||||||
|
|
||||||
expect(described_class.exists?(code: 'en')).to be(false)
|
|
||||||
expect(TagName.where(language_code: 'en', tag_id: [first.id, second.id])).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'accepts a stable locale code outside the historical enum values' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
prepare_english_reference!
|
|
||||||
|
|
||||||
locale = described_class.create!(
|
|
||||||
code: 'en-x-rspec', language_code: 'en',
|
|
||||||
script_code: 'Latn', region_code: 826,
|
|
||||||
variants: 'x-rspec', name: 'English')
|
|
||||||
|
|
||||||
expect(locale.reload).to have_attributes(
|
|
||||||
code: 'en-x-rspec', language_code: 'en', script_code: 'Latn',
|
|
||||||
region_code: 826, variants: 'x-rspec')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'registers the future British English locale tuple' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
|
|
||||||
expect(locale).to have_attributes(code: 'en', language_code: 'en',
|
|
||||||
script_code: 'Latn', region_code: 826,
|
|
||||||
variants: '')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,7 +2,7 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe MaterialExportItem, type: :model do
|
RSpec.describe MaterialExportItem, type: :model do
|
||||||
let(:user) { create(:user, :member) }
|
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
|
let(:material) do
|
||||||
Material.create!(tag:, url: 'https://example.com/material',
|
Material.create!(tag:, url: 'https://example.com/material',
|
||||||
created_by_user: user, updated_by_user: user)
|
created_by_user: user, updated_by_user: user)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ RSpec.describe Post, type: :model do
|
|||||||
expect(post.snapshot_tags_json).to eq([
|
expect(post.snapshot_tags_json).to eq([
|
||||||
{ 'tag_id' => tag.id,
|
{ 'tag_id' => tag.id,
|
||||||
'version_no' => tag.version_no,
|
'version_no' => tag.version_no,
|
||||||
'name' => primary_tag_name_for(tag, 'ja').name,
|
'name' => tag.name,
|
||||||
'category' => tag.category,
|
'category' => tag.category,
|
||||||
'sections' => [{ 'begin_ms' => 2000, 'end_ms' => nil }] },
|
'sections' => [{ 'begin_ms' => 2000, 'end_ms' => nil }] },
|
||||||
{ 'external_tag_id' => external.id }])
|
{ 'external_tag_id' => external.id }])
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe PostVersion, type: :model do
|
RSpec.describe PostVersion, type: :model do
|
||||||
let!(:tag_name) { create(:tag_name, name: 'post_version_spec_tag') }
|
let!(:tag_name) { TagName.create!(name: 'post_version_spec_tag') }
|
||||||
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
|
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
|
||||||
|
|
||||||
let!(:post_record) do
|
let!(:post_record) do
|
||||||
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
|
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Script, type: :model do
|
|
||||||
include_context 'English locale'
|
|
||||||
|
|
||||||
it 'selects locales by script code, not language code' do
|
|
||||||
expect(described_class.find('Jpan').locales).to contain_exactly(Locale.nipponese)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'accepts supported script codes and rejects unsupported ones' do
|
|
||||||
expect(described_class.find('Jpan')).to be_valid
|
|
||||||
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['Jpan'])
|
|
||||||
expect {
|
|
||||||
described_class.create!(code: 'Zzzz', name: 'Unsupported')
|
|
||||||
}.to raise_error { |error|
|
|
||||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not register Latin as a currently supported script' do
|
|
||||||
expect {
|
|
||||||
described_class.create!(code: 'Latn', name: 'Latin')
|
|
||||||
}.to raise_error { |error|
|
|
||||||
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'selects Latin locales when multilingual support is enabled' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
|
|
||||||
expect(described_class.find('Latn').locales).to contain_exactly(locale)
|
|
||||||
expect(described_class.find('Jpan').locales).not_to include(locale)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,8 +2,8 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe TagImplication, type: :model do
|
RSpec.describe TagImplication, type: :model do
|
||||||
it 'rejects a parent tag that would create a cycle' do
|
it 'rejects a parent tag that would create a cycle' do
|
||||||
child = create(:tag, primary_name: 'tag_implication_cycle_child')
|
child = create(:tag, name: 'tag_implication_cycle_child')
|
||||||
parent = create(:tag, primary_name: 'tag_implication_cycle_parent')
|
parent = create(:tag, name: 'tag_implication_cycle_parent')
|
||||||
|
|
||||||
described_class.create!(tag: child, parent_tag: parent)
|
described_class.create!(tag: child, parent_tag: parent)
|
||||||
|
|
||||||
@@ -17,9 +17,9 @@ RSpec.describe TagImplication, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'terminates even when existing data already contains a cycle' do
|
it 'terminates even when existing data already contains a cycle' do
|
||||||
child = create(:tag, primary_name: 'tag_implication_existing_cycle_child')
|
child = create(:tag, name: 'tag_implication_existing_cycle_child')
|
||||||
parent = create(:tag, primary_name: 'tag_implication_existing_cycle_parent')
|
parent = create(:tag, name: 'tag_implication_existing_cycle_parent')
|
||||||
ancestor = create(:tag, primary_name: 'tag_implication_existing_cycle_ancestor')
|
ancestor = create(:tag, name: 'tag_implication_existing_cycle_ancestor')
|
||||||
|
|
||||||
described_class.create!(tag: parent, parent_tag: ancestor)
|
described_class.create!(tag: parent, parent_tag: ancestor)
|
||||||
described_class.insert_all!(
|
described_class.insert_all!(
|
||||||
|
|||||||
@@ -31,48 +31,12 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
|||||||
|
|
||||||
describe '.apply!' do
|
describe '.apply!' do
|
||||||
before do
|
before do
|
||||||
described_class.create!(priority: 10, source_pattern: 'foo_', replacement: 'foo')
|
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
nipponese = 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(nipponese.reload).to have_attributes(name: 'foobar', language_code: 'ja')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renames without merging tags or deleting either language identity' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
nipponese = 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(nipponese.reload, 'ja')).to have_attributes(
|
|
||||||
name: 'foobar', language_code: 'ja', tag_id: nipponese.id)
|
|
||||||
expect(primary_tag_name_for(english.reload, 'en')).to eq(english_name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when no conflicting tag_name exists' do
|
context 'when no conflicting tag_name exists' do
|
||||||
let!(:tag_name) 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)
|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -83,10 +47,10 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a conflicting primary tag name exists' do
|
context 'when a conflicting canonical tag_name exists' do
|
||||||
let!(:existing) { create(:tag_name, name: 'foobar') }
|
let!(:existing) { TagName.create!(name: 'foobar') }
|
||||||
let!(:source) do
|
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)
|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -99,56 +63,48 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when the source tag_name has a tag and the existing one has no tag' do
|
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!(:existing) { TagName.create!(name: 'foobar') }
|
||||||
let!(:source_tag) { create(:tag, primary_name: 'tmp', category: :general) }
|
let!(:source_tag) { create(:tag, name: 'tmp', category: :general) }
|
||||||
let!(:source_tag_name_id) { primary_tag_name_for(source_tag, 'ja').id }
|
let!(:source_tag_name_id) { source_tag.tag_name_id }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
primary_tag_name_for(source_tag, 'ja').update_columns(
|
source_tag.tag_name.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||||
name: 'foo_bar', updated_at: Time.current)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'moves the tag to the existing tag_name' do
|
it 'moves the tag to the existing tag_name' do
|
||||||
described_class.apply!
|
described_class.apply!
|
||||||
expect(existing.reload).to have_attributes(
|
expected_tag_name_id = existing.canonical_id || existing.id
|
||||||
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
|
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
|
||||||
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)
|
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the sanitised name is an alias of an existing tag' do
|
context 'when the sanitised name is an alias of an existing tag' do
|
||||||
let!(:existing_tag) { create(:tag) }
|
let!(:existing_tag) { create(:tag) }
|
||||||
let!(:existing_primary) { primary_tag_name_for(existing_tag, 'ja') }
|
|
||||||
let!(:alias_name) do
|
let!(:alias_name) do
|
||||||
create(:tag_name, :alias, name: 'foobar', tag: existing_tag)
|
TagName.create!(name: 'foobar', canonical: existing_tag.tag_name)
|
||||||
end
|
end
|
||||||
let!(:source) do
|
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)
|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'deletes only the source and preserves the alias and its owning tag' do
|
it 'deletes only the source and preserves the alias and its canonical tag' do
|
||||||
described_class.apply!
|
described_class.apply!
|
||||||
|
|
||||||
expect(TagName.unscoped.exists?(source.id)).to be(false)
|
expect(TagName.unscoped.exists?(source.id)).to be(false)
|
||||||
expect(alias_name.reload).to have_attributes(
|
expect(alias_name.reload.canonical).to eq(existing_tag.tag_name)
|
||||||
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)
|
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when both source and existing tag_names have tags' do
|
context 'when both source and existing tag_names have tags' do
|
||||||
let!(:existing_tn) { create(:tag_name, name: 'foobar') }
|
let!(:existing_tn) { TagName.create!(name: 'foobar') }
|
||||||
let!(:existing_tag) { create(:tag, primary_tag_name: existing_tn, category: :general) }
|
let!(:existing_tag) { Tag.create!(tag_name: existing_tn, category: :general) }
|
||||||
|
|
||||||
let!(:source_tn) { create(:tag_name, name: 'tmp') }
|
let!(:source_tn) { TagName.create!(name: 'tmp') }
|
||||||
let!(:source_tag) { create(:tag, primary_tag_name: source_tn, category: :general) }
|
let!(:source_tag) { Tag.create!(tag_name: source_tn, category: :general) }
|
||||||
let!(:source_tag_name_id) { source_tn.id }
|
let!(:source_tag_name_id) { source_tn.id }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@@ -164,7 +120,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
|||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
expect(TagName.unscoped.exists?(source_tag_name_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(post.reload.tags).to contain_exactly(existing_tag)
|
||||||
expect(primary_tag_name_for(existing_tag.reload, 'ja').name).to eq('foobar')
|
expect(existing_tag.reload.name).to eq('foobar')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe TagName, type: :model do
|
|
||||||
include_context 'English locale'
|
|
||||||
|
|
||||||
it 'allows the same name in different languages' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
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 Nipponese 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,
|
|
||||||
auto_generated: false, tag_id: nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'defaults ordinary persisted human names to confirmed provenance' do
|
|
||||||
name = described_class.create!(name: 'ordinary_human_name', language_code: 'ja',
|
|
||||||
script_code: 'Jpan', primary_flg: true)
|
|
||||||
|
|
||||||
expect(name.reload.auto_generated).to be(false)
|
|
||||||
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 'creates a complete ordinary tag factory under the active Nipponese language' do
|
|
||||||
tag = create(:tag, primary_name: 'factory_multilingual')
|
|
||||||
|
|
||||||
Locale.where(deprecated_at: nil).distinct.pluck(:language_code).each do |language_code|
|
|
||||||
expect(TagName.where(tag:, language_code:, primary_flg: true).count).to eq(1)
|
|
||||||
end
|
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
|
|
||||||
.to contain_exactly(have_attributes(auto_generated: false))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'completes an ordinary tag factory under future active languages' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
tag = create(:tag, primary_name: 'future_factory')
|
|
||||||
|
|
||||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
|
|
||||||
.to contain_exactly(have_attributes(auto_generated: true))
|
|
||||||
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 'does not resolve a detached alias through another detached primary' do
|
|
||||||
create(:tag_name, :alias, name: 'detached_alias')
|
|
||||||
create(:tag_name, name: 'unrelated_detached_primary')
|
|
||||||
|
|
||||||
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
|
|
||||||
.to eq(['detached_alias'])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resolves the primary name of the alias language on the same tag' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
tag = create(:tag, primary_name: '日本語正本')
|
|
||||||
english_primary = primary_tag_name_for(tag, 'en')
|
|
||||||
english_primary.update!(name: 'english_primary', auto_generated: false)
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
nipponese = create(:tag, primary_name: 'nipponese_canonical')
|
|
||||||
english_name = create(:tag_name, name: 'english_canonical',
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
english = create(:tag, primary_tag_name: english_name)
|
|
||||||
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
|
||||||
tag: nipponese)
|
|
||||||
english_alias = create(:tag_name, :alias, name: 'shared_alias',
|
|
||||||
tag: english,
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
|
|
||||||
expect(nipponese_alias.reload).to have_attributes(
|
|
||||||
tag_id: nipponese.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(['nipponese_canonical'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects the generated-name prefix in human input' do
|
|
||||||
human_name = build(:tag_name, name: 'Tag_#999', auto_generated: false)
|
|
||||||
|
|
||||||
expect(human_name).to be_invalid
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows the generated-name prefix for system-created names' do
|
|
||||||
generated_name = build(:tag_name, name: 'Tag_#1000', auto_generated: true)
|
|
||||||
|
|
||||||
expect { generated_name.save! }.to change(described_class, :count).by(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+86
-342
@@ -4,12 +4,12 @@ RSpec.describe Tag, type: :model do
|
|||||||
describe 'external tag separation' do
|
describe 'external tag separation' do
|
||||||
['nico:reserved', 'NiCo:reserved'].each do |name|
|
['nico:reserved', 'NiCo:reserved'].each do |name|
|
||||||
it "rejects the reserved prefix #{ name } for internal tags" do
|
it "rejects the reserved prefix #{ name } for internal tags" do
|
||||||
tag = build(:tag, primary_name: name)
|
tag = build(:tag, name:)
|
||||||
|
|
||||||
expect(tag).to be_invalid
|
expect(tag).to be_invalid
|
||||||
expect(tag.errors[:name]).to be_present
|
expect(tag.errors[:name]).to be_present
|
||||||
expect {
|
expect {
|
||||||
described_class.normalise_tags!(Locale.nipponese, [name])
|
described_class.normalise_tags!([name])
|
||||||
}.to raise_error(Tag::NicoTagNormalisationError)
|
}.to raise_error(Tag::NicoTagNormalisationError)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -25,161 +25,128 @@ RSpec.describe Tag, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '.normalise_tags!' do
|
describe '.normalise_tags!' do
|
||||||
include_context 'English locale'
|
|
||||||
|
|
||||||
it 'canonicalises aliases in the supplied language only' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
prepare_english_reference!
|
|
||||||
nipponese = create(:tag, primary_name: 'nipponese_canonical')
|
|
||||||
english_name = create(:tag_name, name: 'english_canonical',
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
english = create(:tag, primary_tag_name: english_name)
|
|
||||||
primary_tag_name_for(english, 'ja').update!(
|
|
||||||
name: 'english_in_nipponese', auto_generated: false)
|
|
||||||
english_locale = locale
|
|
||||||
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
|
||||||
tag: nipponese)
|
|
||||||
english_alias = create(:tag_name, :alias, name: 'shared_alias',
|
|
||||||
tag: english,
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
|
|
||||||
expect(nipponese_alias.reload).to have_attributes(
|
|
||||||
tag_id: nipponese.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([nipponese])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects deprecated tags when deny_deprecated is enabled' do
|
it 'rejects deprecated tags when deny_deprecated is enabled' do
|
||||||
tag_name = create(:tag_name, name: 'normalise deprecated tag')
|
tag_name = TagName.create!(name: 'normalise deprecated tag')
|
||||||
deprecated_tag = create(:tag,
|
deprecated_tag = Tag.create!(
|
||||||
primary_tag_name: tag_name,
|
tag_name:,
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: 1.day.from_now)
|
deprecated_at: 1.day.from_now
|
||||||
|
)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
described_class.normalise_tags!(Locale.nipponese,
|
described_class.normalise_tags!(
|
||||||
[primary_tag_name_for(deprecated_tag, 'ja').name],
|
[deprecated_tag.name],
|
||||||
deny_deprecated: true)
|
deny_deprecated: true
|
||||||
|
)
|
||||||
}.to raise_error(Tag::DeprecatedTagNormalisationError) { |error|
|
}.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
|
end
|
||||||
|
|
||||||
it 'rejects invalid section literals instead of treating them as zero' do
|
it 'rejects invalid section literals instead of treating them as zero' do
|
||||||
expect {
|
expect {
|
||||||
described_class.normalise_tags!(Locale.nipponese,
|
described_class.normalise_tags!(
|
||||||
['normalise_invalid_section[1:aa-2:00]'],
|
['normalise_invalid_section[1:aa-2:00]'],
|
||||||
with_sections: true)
|
with_sections: true
|
||||||
|
)
|
||||||
}.to raise_error(Tag::SectionLiteralParseError)
|
}.to raise_error(Tag::SectionLiteralParseError)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parses open-ended section literals' do
|
it 'parses open-ended section literals' 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|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parses omitted begin as zero' do
|
it 'parses omitted begin as zero' 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|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
|
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'treats fully open section literals as plain tags' do
|
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|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections)[tag.id]).to be_nil
|
expect(result.fetch(:sections)[tag.id]).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'treats [0:00-] as a plain tag' do
|
it 'treats [0:00-] as a plain tag' do
|
||||||
result = described_class.normalise_tags!(Locale.nipponese,
|
result = described_class.normalise_tags!(
|
||||||
['伊地知ニジカ[0:00-]'],
|
['伊地知ニジカ[0:00-]'],
|
||||||
with_sections: true)
|
with_sections: true
|
||||||
|
)
|
||||||
|
|
||||||
tag = result.fetch(:tags).find do |candidate|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections)[tag.id]).to be_nil
|
expect(result.fetch(:sections)[tag.id]).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'expands zero-width sections to one millisecond' do
|
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]'],
|
['伊地知ニジカ[1:00-1:00]'],
|
||||||
with_sections: true)
|
with_sections: true
|
||||||
|
)
|
||||||
|
|
||||||
tag = result.fetch(:tags).find do |candidate|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
|
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'swaps reversed section boundaries' do
|
it 'swaps reversed section boundaries' do
|
||||||
result = described_class.normalise_tags!(Locale.nipponese,
|
result = described_class.normalise_tags!(
|
||||||
['伊地知ニジカ[2:00-1:00]'],
|
['伊地知ニジカ[2:00-1:00]'],
|
||||||
with_sections: true)
|
with_sections: true
|
||||||
|
)
|
||||||
|
|
||||||
tag = result.fetch(:tags).find do |candidate|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
|
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'merges open-ended sections over later bounded sections' do
|
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]'],
|
['伊地知ニジカ[1:00-][2:00-3:00]'],
|
||||||
with_sections: true)
|
with_sections: true
|
||||||
|
)
|
||||||
|
|
||||||
tag = result.fetch(:tags).find do |candidate|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'merges adjacent bounded and open-ended sections' do
|
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-]'],
|
['伊地知ニジカ[1:00-3:00][3:00-]'],
|
||||||
with_sections: true)
|
with_sections: true
|
||||||
|
)
|
||||||
|
|
||||||
tag = result.fetch(:tags).find do |candidate|
|
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
|
||||||
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
|
|
||||||
end
|
|
||||||
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.expand_parent_tags' do
|
describe '.expand_parent_tags' do
|
||||||
it 'expands through multiple deprecated parents to an active ancestor' do
|
it 'expands through multiple deprecated parents to an active ancestor' do
|
||||||
child = create(:tag, primary_name: 'expand_child')
|
child = create(:tag, name: 'expand_child')
|
||||||
deprecated_parent = create(:tag,
|
deprecated_parent = create(
|
||||||
primary_name: 'expand_deprecated_parent',
|
:tag,
|
||||||
deprecated_at: Time.current)
|
name: 'expand_deprecated_parent',
|
||||||
deprecated_grandparent = create(:tag,
|
deprecated_at: Time.current
|
||||||
primary_name: 'expand_deprecated_grandparent',
|
)
|
||||||
deprecated_at: Time.current)
|
deprecated_grandparent = create(
|
||||||
active_ancestor = create(:tag, primary_name: 'expand_active_ancestor')
|
: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: child, parent_tag: deprecated_parent)
|
||||||
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
|
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
|
||||||
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
|
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
|
||||||
@@ -196,8 +163,8 @@ RSpec.describe Tag, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'terminates when implications contain a cycle' do
|
it 'terminates when implications contain a cycle' do
|
||||||
first = create(:tag, primary_name: 'expand_cycle_first')
|
first = create(:tag, name: 'expand_cycle_first')
|
||||||
second = create(:tag, primary_name: 'expand_cycle_second')
|
second = create(:tag, name: 'expand_cycle_second')
|
||||||
TagImplication.create!(tag: first, parent_tag: second)
|
TagImplication.create!(tag: first, parent_tag: second)
|
||||||
now = Time.current
|
now = Time.current
|
||||||
TagImplication.insert_all!(
|
TagImplication.insert_all!(
|
||||||
@@ -216,268 +183,55 @@ RSpec.describe Tag, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '.find_or_create_by_tag_name!' do
|
describe '.find_or_create_by_tag_name!' do
|
||||||
context 'with an explicit locale' do
|
|
||||||
include_context 'English locale'
|
|
||||||
|
|
||||||
it 'creates the representative name with locale attributes and ownership' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
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 'uses the English primary as the source for a generated Nipponese name' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
allow(TagName).to receive(:generate_name).and_return('generated_nipponese')
|
|
||||||
|
|
||||||
tag = described_class.find_or_create_by_tag_name!(
|
|
||||||
locale, 'english_origin', category: :general)
|
|
||||||
|
|
||||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
|
|
||||||
.to contain_exactly(have_attributes(
|
|
||||||
name: 'english_origin', auto_generated: false))
|
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
|
|
||||||
.to contain_exactly(have_attributes(
|
|
||||||
name: 'generated_nipponese', auto_generated: true))
|
|
||||||
source_tag_name = TagName.find_by!(tag:, language_code: 'en', primary_flg: true)
|
|
||||||
expect(TagName).to have_received(:generate_name)
|
|
||||||
.with(Locale.nipponese, tag, source_tag_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'gives a new tag one primary name for every active language' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
tag = described_class.find_or_create_by_tag_name!(
|
|
||||||
Locale.nipponese, 'new_multilingual_tag', category: :general)
|
|
||||||
|
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
|
||||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates one primary per language when multiple locales use English' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
described_class.find_or_create_by_tag_name!(
|
|
||||||
locale, 'before_second_english_locale', category: :general)
|
|
||||||
Locale.create!(code: 'en-x-rspec', language_code: 'en',
|
|
||||||
script_code: 'Latn', region_code: 826,
|
|
||||||
variants: 'x-rspec', name: 'English')
|
|
||||||
|
|
||||||
tag = described_class.find_or_create_by_tag_name!(
|
|
||||||
locale, 'one_english_primary', category: :general)
|
|
||||||
|
|
||||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
|
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rolls back a new tag and requested name if name generation fails' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
original_tag_count = described_class.count
|
|
||||||
allow(TagName).to receive(:generate_name).and_raise('generation failed')
|
|
||||||
|
|
||||||
expect {
|
|
||||||
described_class.find_or_create_by_tag_name!(
|
|
||||||
locale, 'atomic_english_name', category: :general)
|
|
||||||
}.to raise_error('generation failed')
|
|
||||||
|
|
||||||
expect(described_class.count).to eq(original_tag_count)
|
|
||||||
expect(TagName.where(language_code: 'en', name: 'atomic_english_name')).to be_empty
|
|
||||||
expect(described_class.joins(:tag_names)
|
|
||||||
.where(tag_names: { name: 'atomic_english_name' })).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'keeps a detached requested name detached if generation fails' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
detached = create(:tag_name, :alias, name: 'atomic_detached',
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
original_tag_count = described_class.count
|
|
||||||
allow(TagName).to receive(:generate_name).and_raise('generation failed')
|
|
||||||
|
|
||||||
expect {
|
|
||||||
described_class.find_or_create_by_tag_name!(
|
|
||||||
locale, detached.name, category: :general)
|
|
||||||
}.to raise_error('generation failed')
|
|
||||||
|
|
||||||
expect(described_class.count).to eq(original_tag_count)
|
|
||||||
expect(detached.reload).to have_attributes(tag_id: nil, primary_flg: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'promotes a detached alias into the requested primary name' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
detached = create(:tag_name, :alias, name: 'detached_english',
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
|
|
||||||
tag = described_class.find_or_create_by_tag_name!(
|
|
||||||
locale, detached.name, category: :general)
|
|
||||||
|
|
||||||
expect(detached.reload).to have_attributes(
|
|
||||||
tag_id: tag.id, primary_flg: true, canonical_id: nil,
|
|
||||||
auto_generated: false)
|
|
||||||
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
|
|
||||||
.to contain_exactly(detached)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates separate tag identities for the same name in different languages' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
nipponese = 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(nipponese)
|
|
||||||
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(nipponese, 'ja')).to have_attributes(
|
|
||||||
name: 'same_name', tag_id: nipponese.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(nipponese)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'preserves the V1 representative when looking up another primary language' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
tag = create(:tag, primary_name: '日本語代表名')
|
|
||||||
representative_id = tag.tag_name_id
|
|
||||||
english = primary_tag_name_for(tag, 'en')
|
|
||||||
english.update!(name: 'english_primary', auto_generated: false)
|
|
||||||
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
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
|
it 'creates a tag and name with the requested category after stripping whitespace' do
|
||||||
tag = nil
|
tag = nil
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
tag = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
tag = described_class.find_or_create_by_tag_name!(
|
||||||
' lookup_new ', category: :character)
|
' lookup_new ', category: :character)
|
||||||
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
|
}.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.name).to eq('lookup_new')
|
||||||
expect(tag.category).to eq('character')
|
expect(tag.category).to eq('character')
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
|
|
||||||
expect(primary_tag_name_for(tag, 'ja').auto_generated).to be(false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'promotes a detached Nipponese alias into a human-confirmed primary' do
|
it 'reuses the canonical tag for an alias without changing its category' do
|
||||||
detached = create(:tag_name, :alias, name: 'detached_nipponese')
|
|
||||||
|
|
||||||
tag = described_class.find_or_create_by_tag_name!(
|
|
||||||
Locale.nipponese, detached.name, category: :general)
|
|
||||||
|
|
||||||
expect(detached.reload).to have_attributes(
|
|
||||||
tag_id: tag.id, primary_flg: true, canonical_id: nil,
|
|
||||||
auto_generated: false)
|
|
||||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
|
|
||||||
.to contain_exactly(detached)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'reuses the owning tag for an alias without changing its category' do
|
|
||||||
tag = create(:tag, category: :character)
|
tag = create(:tag, category: :character)
|
||||||
representative_id = tag.tag_name_id
|
alias_name = TagName.create!(name: 'lookup_alias', canonical: tag.tag_name)
|
||||||
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
|
found = nil
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
found = described_class.find_or_create_by_tag_name!(
|
||||||
alias_name.name, category: :general)
|
alias_name.name, category: :general)
|
||||||
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
|
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
|
||||||
|
|
||||||
expect(found).to eq(tag)
|
expect(found).to eq(tag)
|
||||||
expect(found.category).to eq('character')
|
expect(found.category).to eq('character')
|
||||||
expect(tag.reload.tag_name_id).to eq(representative_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'reuses the owning tag through another alias' do
|
it 'creates a tag for an existing canonical name reached through an alias' do
|
||||||
tag = create(:tag)
|
canonical = create(:tag_name)
|
||||||
primary = primary_tag_name_for(tag, 'ja')
|
alias_name = TagName.create!(name: 'lookup_alias', canonical:)
|
||||||
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
|
tag = nil
|
||||||
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 {
|
expect {
|
||||||
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
|
tag = described_class.find_or_create_by_tag_name!(
|
||||||
alias_name.name, category: :general)
|
alias_name.name, category: :general)
|
||||||
expect(found).to eq(tag)
|
}.to change(Tag, :count).by(1).and change(TagName, :count).by(0)
|
||||||
}.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.tag_name).to eq(canonical)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.merge_tags!' do
|
describe '.merge_tags!' do
|
||||||
let!(:target_tag) { create(:tag, category: :general) }
|
let!(:target_tag) { create(:tag, category: :general) }
|
||||||
let!(:source_tag) { create(:tag, category: :general) }
|
let!(:source_tag) { create(:tag, category: :general) }
|
||||||
let!(:source_tag_name) { primary_tag_name_for(source_tag, 'ja') }
|
let!(:source_tag_name) { source_tag.tag_name }
|
||||||
|
|
||||||
let!(:post_record) do
|
let!(:post_record) do
|
||||||
Post.create!(url: 'https://example.com/posts/1', title: 'test post')
|
Post.create!(url: 'https://example.com/posts/1', title: 'test post')
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with names in multiple languages' do
|
|
||||||
include_context 'English locale'
|
|
||||||
|
|
||||||
it 'keeps the target primary and moves source names as aliases' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
english_primary = primary_tag_name_for(target_tag, 'en')
|
|
||||||
source_primary = primary_tag_name_for(source_tag, 'en')
|
|
||||||
source_alias = create(:tag_name, :alias, tag: source_tag,
|
|
||||||
name: 'source_english_alias', language_code: 'en', script_code: 'Latn')
|
|
||||||
|
|
||||||
expect([english_primary, source_primary]).to all(have_attributes(
|
|
||||||
primary_flg: true, auto_generated: true))
|
|
||||||
|
|
||||||
described_class.merge_tags!(target_tag, [source_tag])
|
|
||||||
|
|
||||||
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(english_primary)
|
|
||||||
expect(TagName.where(tag: target_tag, language_code: 'en', primary_flg: true))
|
|
||||||
.to contain_exactly(english_primary)
|
|
||||||
expect([source_primary.reload, source_alias.reload]).to all(have_attributes(
|
|
||||||
tag_id: target_tag.id, language_code: 'en', primary_flg: false))
|
|
||||||
expect(TagName.where(tag: target_tag, language_code: 'ja', primary_flg: true))
|
|
||||||
.to contain_exactly(primary_tag_name_for(target_tag, 'ja'))
|
|
||||||
expect(source_tag_name.reload).to have_attributes(
|
|
||||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when merging a simple source tag' do
|
context 'when merging a simple source tag' do
|
||||||
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
|
||||||
|
|
||||||
@@ -489,8 +243,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
||||||
expect(target_link).to be_present
|
expect(target_link).to be_present
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
expect(source_tag_name.reload).to have_attributes(
|
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -516,16 +269,15 @@ RSpec.describe Tag, type: :model do
|
|||||||
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
|
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
expect(source_tag_name.reload).to have_attributes(
|
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps source history and records the new target alias after deleting the source' do
|
it 'keeps source history and records the new target alias after deleting the source' do
|
||||||
user = create_member_user!
|
user = create_member_user!
|
||||||
source_name = primary_tag_name_for(source_tag, 'ja').name
|
source_name = source_tag.name
|
||||||
source_alias = create(:tag_name, :alias, name: 'merge_alias', tag: source_tag)
|
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name)
|
||||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
|
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
|
||||||
original_version = source_tag.tag_versions.first
|
original_version = source_tag.tag_versions.first
|
||||||
|
|
||||||
@@ -535,17 +287,13 @@ RSpec.describe Tag, type: :model do
|
|||||||
expect(versions.pluck(:version_no, :event_type))
|
expect(versions.pluck(:version_no, :event_type))
|
||||||
.to eq([[1, 'create'], [2, 'discard']])
|
.to eq([[1, 'create'], [2, 'discard']])
|
||||||
expect(versions.first).to eq(original_version)
|
expect(versions.first).to eq(original_version)
|
||||||
expect(versions.last.created_by_user).to eq(user)
|
expect(versions.last).to have_attributes(
|
||||||
expect(versions.last.names.fetch('ja')).to eq(
|
name: source_name, aliases: source_alias.name, created_by_user: user)
|
||||||
'name' => source_name, 'script_code' => 'Jpan',
|
|
||||||
'auto_generated' => false,
|
|
||||||
'aliases' => [{ 'name' => source_alias.name, 'script_code' => 'Jpan' }])
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
|
|
||||||
target_versions = target_tag.tag_versions.order(:version_no)
|
target_versions = target_tag.tag_versions.order(:version_no)
|
||||||
expect(target_versions.pluck(:event_type)).to eq(['create', 'update'])
|
expect(target_versions.pluck(:event_type)).to eq(['create', 'update'])
|
||||||
target_aliases = target_versions.last.names.fetch('ja').fetch('aliases').pluck('name')
|
expect(target_versions.last.aliases.split).to eq([source_name])
|
||||||
expect(target_aliases).to contain_exactly('merge_alias', source_name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'deletes source relationships while preserving unrelated relationships' do
|
it 'deletes source relationships while preserving unrelated relationships' do
|
||||||
@@ -599,8 +347,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
|
||||||
expect(target_link).to be_present
|
expect(target_link).to be_present
|
||||||
expect(source_tag_name.reload).to have_attributes(
|
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -631,8 +378,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
expect(target_link).to be_present
|
expect(target_link).to be_present
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
|
||||||
expect(source_tag_name.reload).to have_attributes(
|
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
|
||||||
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
|
|
||||||
expect(target_tag.reload.post_count).to eq(1)
|
expect(target_tag.reload.post_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -651,7 +397,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
|
|
||||||
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do
|
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do
|
||||||
earlier_source = create(:tag)
|
earlier_source = create(:tag)
|
||||||
earlier_name = primary_tag_name_for(earlier_source, 'ja')
|
earlier_name = earlier_source.tag_name
|
||||||
source_section = create(:post_tag_section, post: post_record,
|
source_section = create(:post_tag_section, post: post_record,
|
||||||
tag: source_tag,
|
tag: source_tag,
|
||||||
begin_ms: 1000, end_ms: 2000)
|
begin_ms: 1000, end_ms: 2000)
|
||||||
@@ -661,8 +407,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
}.to raise_error(ActiveRecord::RecordInvalid)
|
}.to raise_error(ActiveRecord::RecordInvalid)
|
||||||
|
|
||||||
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
|
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
|
||||||
expect(earlier_name.reload).to have_attributes(
|
expect(earlier_name.reload.canonical_id).to be_nil
|
||||||
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]))
|
expect(TagVersion.where(tag_id: [earlier_source.id, source_tag.id, target_tag.id]))
|
||||||
.to be_empty
|
.to be_empty
|
||||||
expect(Tag.unscoped.exists?(source_tag.id)).to be(true)
|
expect(Tag.unscoped.exists?(source_tag.id)).to be(true)
|
||||||
@@ -671,8 +416,7 @@ RSpec.describe Tag, type: :model do
|
|||||||
expect(source_post_tag.sections).to contain_exactly(source_section)
|
expect(source_post_tag.sections).to contain_exactly(source_section)
|
||||||
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
|
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.reload.post_count).to eq(1)
|
||||||
expect(source_tag_name.reload).to have_attributes(
|
expect(source_tag_name.reload.canonical_id).to be_nil
|
||||||
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
|
|
||||||
expect(target_tag.reload.post_count).to eq(0)
|
expect(target_tag.reload.post_count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe VersionRecord, type: :model do
|
RSpec.describe VersionRecord, type: :model do
|
||||||
let!(:tag) { create(:tag, primary_name: 'version_record_tag') }
|
let!(:tag) { create(:tag, name: 'version_record_tag') }
|
||||||
let!(:nico_tag) { create(:external_tag, name: 'version_record_tag') }
|
let!(:nico_tag) { create(:external_tag, name: 'version_record_tag') }
|
||||||
|
|
||||||
it 'makes TagVersion read only after create' do
|
it 'makes TagVersion read only after create' do
|
||||||
@@ -9,14 +9,13 @@ RSpec.describe VersionRecord, type: :model do
|
|||||||
tag: tag,
|
tag: tag,
|
||||||
version_no: 1,
|
version_no: 1,
|
||||||
event_type: 'create',
|
event_type: 'create',
|
||||||
name: primary_tag_name_for(tag, 'ja').name,
|
name: tag.name,
|
||||||
category: tag.category,
|
category: tag.category,
|
||||||
aliases: '',
|
aliases: '',
|
||||||
names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
|
|
||||||
script_code: 'Jpan', auto_generated: false, aliases: [] } },
|
|
||||||
parent_tag_ids: '',
|
parent_tag_ids: '',
|
||||||
created_at: Time.current,
|
created_at: Time.current,
|
||||||
created_by_user: nil)
|
created_by_user: nil
|
||||||
|
)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
version.update!(name: 'changed')
|
version.update!(name: 'changed')
|
||||||
@@ -28,14 +27,13 @@ RSpec.describe VersionRecord, type: :model do
|
|||||||
tag: tag,
|
tag: tag,
|
||||||
version_no: 1,
|
version_no: 1,
|
||||||
event_type: 'create',
|
event_type: 'create',
|
||||||
name: primary_tag_name_for(tag, 'ja').name,
|
name: tag.name,
|
||||||
category: tag.category,
|
category: tag.category,
|
||||||
aliases: '',
|
aliases: '',
|
||||||
names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
|
|
||||||
script_code: 'Jpan', auto_generated: false, aliases: [] } },
|
|
||||||
parent_tag_ids: '',
|
parent_tag_ids: '',
|
||||||
created_at: Time.current,
|
created_at: Time.current,
|
||||||
created_by_user: nil)
|
created_by_user: nil
|
||||||
|
)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
version.destroy!
|
version.destroy!
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ RSpec.describe 'Deerjikists API', type: :request do
|
|||||||
|
|
||||||
expect(json['tag']).to be_a(Hash)
|
expect(json['tag']).to be_a(Hash)
|
||||||
expect(json['tag']['id']).to eq(tag1.id)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ RSpec.describe 'error responses', type: :request do
|
|||||||
|
|
||||||
it 'returns a stable field-error payload for unprocessable requests' do
|
it 'returns a stable field-error payload for unprocessable requests' do
|
||||||
member = create(:user, :member)
|
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)
|
sign_in_as(member)
|
||||||
|
|
||||||
patch "/tags/#{ tag.id }", params: { category: 'nico' }
|
patch "/tags/#{ tag.id }", params: { category: 'nico' }
|
||||||
|
|||||||
@@ -898,22 +898,23 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
|||||||
|
|
||||||
describe 'GET /gekanator/questions' do
|
describe 'GET /gekanator/questions' do
|
||||||
it 'omits questions for deprecated tags' do
|
it 'omits questions for deprecated tags' do
|
||||||
active_tag = create(:tag, primary_name: 'active_question_tag', category: :general)
|
active_tag = Tag.create!(name: 'active_question_tag', category: :general)
|
||||||
deprecated_tag = create(:tag,
|
deprecated_tag = Tag.create!(
|
||||||
primary_name: 'deprecated_question_tag',
|
name: 'deprecated_question_tag',
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
[active_tag, deprecated_tag].each do |question_tag|
|
[active_tag, deprecated_tag].each do |question_tag|
|
||||||
GekanatorQuestion.create!(
|
GekanatorQuestion.create!(
|
||||||
text: "#{ primary_tag_name_for(question_tag, 'ja').name }?",
|
text: "#{ question_tag.name }?",
|
||||||
kind: 'tag',
|
kind: 'tag',
|
||||||
source: 'admin_curated',
|
source: 'admin_curated',
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
priority_weight: 1.0,
|
priority_weight: 1.0,
|
||||||
condition: {
|
condition: {
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
key: "#{ question_tag.category }:#{ primary_tag_name_for(question_tag, 'ja').name }"
|
key: "#{ question_tag.category }:#{ question_tag.name }"
|
||||||
},
|
},
|
||||||
created_by: admin
|
created_by: admin
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ require 'rails_helper'
|
|||||||
RSpec.describe 'Gekanator posts API', type: :request do
|
RSpec.describe 'Gekanator posts API', type: :request do
|
||||||
describe 'GET /gekanator/posts' do
|
describe 'GET /gekanator/posts' do
|
||||||
it 'omits deprecated tags and returns the stored similarity cosine' do
|
it 'omits deprecated tags and returns the stored similarity cosine' do
|
||||||
active_tag = create(:tag, primary_name: 'active tag', category: :general)
|
active_tag = Tag.create!(name: 'active tag', category: :general)
|
||||||
deprecated_tag = create(:tag,
|
deprecated_tag = Tag.create!(
|
||||||
primary_name: 'deprecated tag',
|
name: 'deprecated tag',
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current
|
||||||
|
)
|
||||||
post_record = Post.create!(title: 'source', url: 'https://example.com/source')
|
post_record = Post.create!(title: 'source', url: 'https://example.com/source')
|
||||||
target_post = Post.create!(title: 'target', url: 'https://example.com/target')
|
target_post = Post.create!(title: 'target', url: 'https://example.com/target')
|
||||||
|
|
||||||
|
|||||||
@@ -1,363 +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
|
|
||||||
|
|
||||||
it 'confirms a generated Nipponese primary when a human renames it' do
|
|
||||||
tag = create(:tag, primary_name: 'generated_source')
|
|
||||||
primary = primary_tag_name_for(tag, 'ja')
|
|
||||||
primary.update!(name: "Tag_##{ tag.id }", auto_generated: true)
|
|
||||||
|
|
||||||
patch "/tags/#{ tag.id }", params: { locale: 'ja', name: '人間が確定した名称' }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok), response.body
|
|
||||||
expect(primary.reload).to have_attributes(
|
|
||||||
name: '人間が確定した名称', auto_generated: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rolls back Nipponese edits when an alias conflicts with another primary' do
|
|
||||||
tag = create(:tag, primary_name: 'target_before_collision')
|
|
||||||
create(:tag_name, :alias, tag:, name: 'existing_alias')
|
|
||||||
other_tag = create(:tag, primary_name: 'other_primary')
|
|
||||||
other_primary = primary_tag_name_for(other_tag, 'ja')
|
|
||||||
primary_name_before = primary_tag_name_for(tag, 'ja').name
|
|
||||||
aliases_before = tag.tag_names.where(primary_flg: false).order(:name).pluck(:name)
|
|
||||||
category_before = tag.category
|
|
||||||
|
|
||||||
put "/tags/#{ tag.id }", params: {
|
|
||||||
locale: 'ja', name: 'renamed_before_collision', category: 'character',
|
|
||||||
deprecated: false, aliases: 'other_primary', parent_tags: '' }
|
|
||||||
|
|
||||||
aggregate_failures do
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
|
||||||
expect(other_primary.reload).to have_attributes(tag_id: other_tag.id, primary_flg: true)
|
|
||||||
expect(TagName.where(tag: other_tag, language_code: 'ja', primary_flg: true))
|
|
||||||
.to contain_exactly(other_primary)
|
|
||||||
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq(primary_name_before)
|
|
||||||
expect(tag.category).to eq(category_before)
|
|
||||||
expect(tag.tag_names.where(primary_flg: false).order(:name).pluck(:name))
|
|
||||||
.to eq(aliases_before)
|
|
||||||
end
|
|
||||||
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 Nipponese fallback' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
|
|
||||||
locale if requested_locale
|
|
||||||
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 Nipponese fallback' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
|
|
||||||
locale if requested_locale
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
|
|
||||||
locale if requested_locale
|
|
||||||
# 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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
|
|
||||||
locale if requested_locale
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
|
|
||||||
locale if requested_locale
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
|
|
||||||
locale if requested_locale
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
|
|
||||||
locale if requested_locale
|
|
||||||
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(:nipponese_tag) do
|
|
||||||
create(:tag, primary_name: 'temporary_nipponese', 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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
english_tag
|
|
||||||
nipponese_tag
|
|
||||||
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(nipponese_tag)
|
|
||||||
expect(english_tag.reload.category).to eq('material')
|
|
||||||
expect(nipponese_tag.reload.category).to eq('general')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the post using the English identity' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
english_tag
|
|
||||||
nipponese_tag
|
|
||||||
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(nipponese_tag)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resolves the English material tag' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
english_tag
|
|
||||||
nipponese_tag
|
|
||||||
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: nipponese_tag)).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resolves the English parent tag' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
english_tag
|
|
||||||
nipponese_tag
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
english_tag
|
|
||||||
nipponese_tag
|
|
||||||
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 'marks a human-renamed generated primary as no longer auto-generated' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
generated_tag = create(:tag, primary_name: 'rename_generated_source')
|
|
||||||
generated_name = primary_tag_name_for(generated_tag, 'en')
|
|
||||||
expect(generated_name.auto_generated).to be(true)
|
|
||||||
|
|
||||||
patch "/tags/#{ generated_tag.id }", params: {
|
|
||||||
locale: 'en', name: 'human_english_name' }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok), response.body
|
|
||||||
expect(generated_name.reload).to have_attributes(
|
|
||||||
name: 'human_english_name', auto_generated: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not steal another tag primary for an alias' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
other_tag = create(:tag, primary_name: 'other_nipponese')
|
|
||||||
other_primary = primary_tag_name_for(other_tag, 'en')
|
|
||||||
other_primary.update!(name: 'other_english_primary', auto_generated: false)
|
|
||||||
create(:tag_name, :alias, tag:, name: 'existing_english_alias',
|
|
||||||
language_code: 'en', script_code: 'Latn')
|
|
||||||
primary_name_before = primary_tag_name_for(tag, 'en').name
|
|
||||||
aliases_before =
|
|
||||||
tag.tag_names.where(language_code: 'en', primary_flg: false)
|
|
||||||
.order(:name).pluck(:name)
|
|
||||||
category_before = tag.category
|
|
||||||
|
|
||||||
put "/tags/#{ tag.id }", params: {
|
|
||||||
locale: 'en', name: 'renamed_before_alias_collision',
|
|
||||||
category: 'character', deprecated: false,
|
|
||||||
aliases: 'other_english_primary', parent_tags: '' }
|
|
||||||
|
|
||||||
aggregate_failures do
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
|
||||||
expect(other_primary.reload).to have_attributes(
|
|
||||||
tag_id: other_tag.id, language_code: 'en', primary_flg: true)
|
|
||||||
expect(TagName.where(tag: other_tag, language_code: 'en', primary_flg: true))
|
|
||||||
.to contain_exactly(other_primary)
|
|
||||||
expect(primary_tag_name_for(tag.reload, 'en').name).to eq(primary_name_before)
|
|
||||||
expect(tag.category).to eq(category_before)
|
|
||||||
aliases_after =
|
|
||||||
tag.tag_names.where(language_code: 'en', primary_flg: false)
|
|
||||||
.order(:name).pluck(:name)
|
|
||||||
expect(aliases_after).to eq(aliases_before)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows a rename to a name already used in Nipponese' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
nipponese = 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(nipponese.reload, 'ja')).to have_attributes(
|
|
||||||
name: 'rename_target', language_code: 'ja', tag_id: nipponese.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates aliases in the requested language' do
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
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
|
|
||||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
|
||||||
locale
|
|
||||||
nipponese = create(:tag, primary_name: 'nipponese_owner')
|
|
||||||
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
|
||||||
tag: nipponese)
|
|
||||||
expect(nipponese_alias.reload).to have_attributes(
|
|
||||||
tag_id: nipponese.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(nipponese_alias.reload).to have_attributes(
|
|
||||||
tag_id: nipponese.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', nipponese.id], ['en', tag.id])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -26,10 +26,10 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
|
|
||||||
describe 'GET /materials' do
|
describe 'GET /materials' do
|
||||||
let!(:tag_a) 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
|
end
|
||||||
let!(:tag_b) do
|
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
|
end
|
||||||
|
|
||||||
let!(:material_a) do
|
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
|
it 'filters by descendant tags and returns stable parent tag groups' do
|
||||||
root =
|
root =
|
||||||
create(:tag, primary_name: 'material_scope_root',
|
Tag.create!(tag_name: TagName.create!(name: 'material_scope_root'),
|
||||||
category: :material)
|
category: :material)
|
||||||
child_b =
|
child_b =
|
||||||
create(:tag, primary_name: 'material_scope_b',
|
Tag.create!(tag_name: TagName.create!(name: 'material_scope_b'),
|
||||||
category: :material)
|
category: :material)
|
||||||
child_a =
|
child_a =
|
||||||
create(:tag, primary_name: 'material_scope_a',
|
Tag.create!(tag_name: TagName.create!(name: 'material_scope_a'),
|
||||||
category: :material)
|
category: :material)
|
||||||
deprecated =
|
deprecated =
|
||||||
create(:tag, primary_name: 'material_scope_old',
|
Tag.create!(tag_name: TagName.create!(name: 'material_scope_old'),
|
||||||
category: :material,
|
category: :material,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current)
|
||||||
grandchild =
|
grandchild =
|
||||||
create(:tag, primary_name: 'material_scope_grandchild',
|
Tag.create!(tag_name: TagName.create!(name: 'material_scope_grandchild'),
|
||||||
category: :material)
|
category: :material)
|
||||||
root_material =
|
root_material =
|
||||||
build_material(tag: root, user: member_user,
|
build_material(tag: root, user: member_user,
|
||||||
@@ -179,7 +179,7 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
|
|
||||||
describe 'GET /materials/:id' do
|
describe 'GET /materials/:id' do
|
||||||
let!(:tag) do
|
let!(:tag) do
|
||||||
create(:tag, primary_name: 'material_show', category: :material)
|
Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material)
|
||||||
end
|
end
|
||||||
let!(:material) do
|
let!(:material) do
|
||||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
|
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)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
material = Material.order(:id).last
|
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.tag.category).to eq('material')
|
||||||
expect(material.created_by_user).to eq(member_user)
|
expect(material.created_by_user).to eq(member_user)
|
||||||
expect(material.updated_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)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
tag = TagName.find_by!(name: 'material_create_versioned_tag',
|
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_create_versioned_tag' })
|
||||||
language_code: 'ja', primary_flg: true).tag
|
|
||||||
version = tag.tag_versions.order(:version_no).last
|
version = tag.tag_versions.order(:version_no).last
|
||||||
|
|
||||||
expect(version.event_type).to eq('create')
|
expect(version.event_type).to eq('create')
|
||||||
@@ -324,8 +323,8 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 422 when the existing tag is not material/character' do
|
it 'returns 422 when the existing tag is not material/character' do
|
||||||
general_tag_name = create(:tag_name, name: 'material_create_general_tag')
|
general_tag_name = TagName.create!(name: 'material_create_general_tag')
|
||||||
create(:tag, primary_tag_name: general_tag_name, category: :general)
|
Tag.create!(tag_name: general_tag_name, category: :general)
|
||||||
|
|
||||||
post '/materials', params: {
|
post '/materials', params: {
|
||||||
tag: 'material_create_general_tag',
|
tag: 'material_create_general_tag',
|
||||||
@@ -346,7 +345,7 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
material = Material.order(:id).last
|
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.url).to eq('https://example.com/material-source')
|
||||||
expect(material.file.attached?).to be(false)
|
expect(material.file.attached?).to be(false)
|
||||||
end
|
end
|
||||||
@@ -385,7 +384,7 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
|
|
||||||
describe 'PUT /materials/:id' do
|
describe 'PUT /materials/:id' do
|
||||||
let!(:tag) 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
|
end
|
||||||
let!(:material) do
|
let!(:material) do
|
||||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
|
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)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
material.reload
|
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.tag.category).to eq('material')
|
||||||
expect(material.url).to eq('https://example.com/updated-source')
|
expect(material.url).to eq('https://example.com/updated-source')
|
||||||
expect(material.updated_by_user).to eq(member_user)
|
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)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
tag = TagName.find_by!(name: 'material_update_versioned_tag',
|
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_update_versioned_tag' })
|
||||||
language_code: 'ja', primary_flg: true).tag
|
|
||||||
version = tag.tag_versions.order(:version_no).last
|
version = tag.tag_versions.order(:version_no).last
|
||||||
|
|
||||||
expect(version.event_type).to eq('create')
|
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
|
it 'backfills a create tag_version for an existing material tag without history' do
|
||||||
existing_tag =
|
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)
|
category: :material)
|
||||||
|
|
||||||
expect(existing_tag.tag_versions).to be_empty
|
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
|
it 'backfills a create tag_version for an existing character tag without history' do
|
||||||
existing_tag =
|
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)
|
category: :character)
|
||||||
|
|
||||||
expect(existing_tag.tag_versions).to be_empty
|
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)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
material.reload
|
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.url).to eq('https://example.com/updated-source')
|
||||||
expect(material.updated_by_user).to eq(member_user)
|
expect(material.updated_by_user).to eq(member_user)
|
||||||
expect(material.file.attached?).to be(false)
|
expect(material.file.attached?).to be(false)
|
||||||
@@ -653,8 +651,8 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /materials/download.zip' do
|
describe 'GET /materials/download.zip' do
|
||||||
let!(:tag_a) { create(:tag, primary_name: 'zip_a', category: :material) }
|
let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'zip_a'), category: :material) }
|
||||||
let!(:tag_b) { create(:tag, primary_name: 'zip_b', category: :material) }
|
let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'zip_b'), category: :material) }
|
||||||
let!(:material_a) do
|
let!(:material_a) do
|
||||||
build_material(tag: tag_a, user: member_user,
|
build_material(tag: tag_a, user: member_user,
|
||||||
file: dummy_upload(filename: 'a.png', body: 'zip-a'))
|
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
|
describe 'GET /materials/versions' do
|
||||||
let!(:tag) do
|
let!(:tag) do
|
||||||
create(:tag, primary_name: 'material_history', category: :material)
|
Tag.create!(tag_name: TagName.create!(name: 'material_history'), category: :material)
|
||||||
end
|
end
|
||||||
let!(:material) do
|
let!(:material) do
|
||||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'history.png'))
|
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
|
describe 'DELETE /materials/:id' do
|
||||||
let!(:tag) do
|
let!(:tag) do
|
||||||
create(:tag, primary_name: 'material_destroy', category: :material)
|
Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material)
|
||||||
end
|
end
|
||||||
let!(:material) do
|
let!(:material) do
|
||||||
build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
|
build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ RSpec.describe 'NicoTags', type: :request do
|
|||||||
unlinked.update!(name: 'search_unlinked')
|
unlinked.update!(name: 'search_unlinked')
|
||||||
other = create(:external_tag)
|
other = create(:external_tag)
|
||||||
other.update!(name: 'other')
|
other.update!(name: 'other')
|
||||||
destination = create(:tag, category: :general)
|
destination = create(:tag, :general)
|
||||||
primary_tag_name_for(destination, 'ja').update!(name: 'destination_search')
|
destination.tag_name.update!(name: 'destination_search')
|
||||||
NicoTagRelation.create!(nico_tag: linked, tag: destination)
|
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: {
|
get '/tags/nico', params: {
|
||||||
name: 'search_',
|
name: 'search_',
|
||||||
@@ -151,7 +151,7 @@ RSpec.describe 'NicoTags', type: :request do
|
|||||||
|
|
||||||
it '404 when only an internal tag exists for the target id' do
|
it '404 when only an internal tag exists for the target id' do
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
non_nico = create(:tag, category: :general)
|
non_nico = create(:tag, :general)
|
||||||
expect(ExternalTag.exists?(non_nico.id)).to be(false)
|
expect(ExternalTag.exists?(non_nico.id)).to be(false)
|
||||||
patch "/tags/nico/#{non_nico.id}", params: { tags: 'a b' }
|
patch "/tags/nico/#{non_nico.id}", params: { tags: 'a b' }
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
@@ -162,18 +162,18 @@ RSpec.describe 'NicoTags', type: :request do
|
|||||||
|
|
||||||
nico_tag = create(:external_tag, name: 'nico_tags_spec_source')
|
nico_tag = create(:external_tag, name: 'nico_tags_spec_source')
|
||||||
|
|
||||||
linked_a_name = create(:tag_name, name: 'nico_linked_a')
|
linked_a_name = TagName.create!(name: 'nico_linked_a')
|
||||||
linked_a = create(:tag, primary_tag_name: linked_a_name, category: :general)
|
linked_a = Tag.create!(tag_name: linked_a_name, category: :general)
|
||||||
|
|
||||||
linked_b_name = create(:tag_name, name: 'nico_linked_b')
|
linked_b_name = TagName.create!(name: 'nico_linked_b')
|
||||||
linked_b = create(:tag, primary_tag_name: linked_b_name, category: :general)
|
linked_b = Tag.create!(tag_name: linked_b_name, category: :general)
|
||||||
|
|
||||||
NicoTagVersionRecorder.record!(external_tag: nico_tag,
|
NicoTagVersionRecorder.record!(external_tag: nico_tag,
|
||||||
event_type: :create, created_by_user: admin)
|
event_type: :create, created_by_user: admin)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
patch "/tags/nico/#{nico_tag.id}", params: {
|
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)
|
}.to change(TagVersion, :count).by(2)
|
||||||
.and change(NicoTagVersion, :count).by(1)
|
.and change(NicoTagVersion, :count).by(1)
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
'sections' => [])
|
'sections' => [])
|
||||||
end
|
end
|
||||||
|
|
||||||
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
|
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
|
||||||
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
|
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
|
||||||
|
|
||||||
let!(:post_record) do
|
let!(:post_record) do
|
||||||
Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
|
Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
|
||||||
@@ -125,11 +125,11 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
describe 'GET /posts' do
|
describe 'GET /posts' do
|
||||||
let!(:user) { create_member_user! }
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
|
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
|
||||||
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
|
let!(:tag) { Tag.create!(tag_name:, category: :general) }
|
||||||
let!(:tag_name2) { create(:tag_name, name: 'unko') }
|
let!(:tag_name2) { TagName.create!(name: 'unko') }
|
||||||
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :deerjikist) }
|
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) }
|
||||||
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
|
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
|
||||||
|
|
||||||
let!(:hit_post) do
|
let!(:hit_post) do
|
||||||
Post.create!(uploaded_user: user, title: 'hello spec world',
|
Post.create!(uploaded_user: user, title: 'hello spec world',
|
||||||
@@ -267,7 +267,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps internal name searches independent of colliding external ids' do
|
it 'keeps internal name searches independent of colliding external ids' do
|
||||||
get '/posts', params: { tags: primary_tag_name_for(tag, 'ja').name }
|
get '/posts', params: { tags: tag.name }
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json.fetch('count')).to eq(3)
|
expect(json.fetch('count')).to eq(3)
|
||||||
@@ -286,7 +286,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
[nil, 'all'].each do |match|
|
[nil, 'all'].each do |match|
|
||||||
it "intersects internal and external matches with match=#{ match || 'omitted' }" do
|
it "intersects internal and external matches with match=#{ match || 'omitted' }" do
|
||||||
params = { tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external" }
|
params = { tags: "#{ tag.name } nico:search_external" }
|
||||||
params[:match] = match if match
|
params[:match] = match if match
|
||||||
|
|
||||||
get '/posts', params: params
|
get '/posts', params: params
|
||||||
@@ -307,8 +307,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'excludes external matches from an internal tag search' do
|
it 'excludes external matches from an internal tag search' do
|
||||||
get '/posts', params: {
|
get '/posts', params: { tags: "#{ tag.name } not:nico:search_external" }
|
||||||
tags: "#{ primary_tag_name_for(tag, 'ja').name } not:nico:search_external" }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json.fetch('count')).to eq(2)
|
expect(json.fetch('count')).to eq(2)
|
||||||
@@ -317,9 +316,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'unions an external match with a negated internal match' do
|
it 'unions an external match with a negated internal match' do
|
||||||
get '/posts', params: {
|
get '/posts', params: { tags: "nico:search_external not:#{ tag.name }", match: 'any' }
|
||||||
tags: "nico:search_external not:#{ primary_tag_name_for(tag, 'ja').name }",
|
|
||||||
match: 'any' }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json.fetch('count')).to eq(2)
|
expect(json.fetch('count')).to eq(2)
|
||||||
@@ -336,9 +333,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'applies the same mixed name search to the existing random endpoint' do
|
it 'applies the same mixed name search to the existing random endpoint' do
|
||||||
get '/posts/random', params: {
|
get '/posts/random', params: { tags: "#{ tag.name } nico:search_external", match: 'all' }
|
||||||
tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external",
|
|
||||||
match: 'all' }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json.fetch('id')).to eq(both_post.id)
|
expect(json.fetch('id')).to eq(both_post.id)
|
||||||
@@ -346,17 +341,17 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when tags contain not:' do
|
context 'when tags contain not:' do
|
||||||
let!(:foo_tag_name) { create(:tag_name, name: 'not_spec_foo') }
|
let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') }
|
||||||
let!(:foo_tag) { create(:tag, primary_tag_name: foo_tag_name, category: :general) }
|
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_name) { TagName.create!(name: 'not_spec_bar') }
|
||||||
let!(:bar_tag) { create(:tag, primary_tag_name: bar_tag_name, category: :general) }
|
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_name) { TagName.create!(name: 'not_spec_baz') }
|
||||||
let!(:baz_tag) { create(:tag, primary_tag_name: baz_tag_name, category: :general) }
|
let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) }
|
||||||
|
|
||||||
let!(:foo_alias_tag_name) do
|
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
|
end
|
||||||
|
|
||||||
let!(:foo_only_post) do
|
let!(:foo_only_post) do
|
||||||
@@ -744,9 +739,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
.find { _1['name'] == 'nico:post_index_external' }
|
.find { _1['name'] == 'nico:post_index_external' }
|
||||||
|
|
||||||
expect(post_json.fetch('tags')).to include(
|
expect(post_json.fetch('tags')).to include(
|
||||||
a_hash_including('id' => tag.id,
|
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'))
|
||||||
'name' => primary_tag_name_for(tag, 'ja').name,
|
|
||||||
'category' => 'general'))
|
|
||||||
expect(external_json).not_to be_nil
|
expect(external_json).not_to be_nil
|
||||||
expect_external_tag_json(external_json, external_tag)
|
expect_external_tag_json(external_json, external_tag)
|
||||||
end
|
end
|
||||||
@@ -779,10 +772,11 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'omits deprecated tags' do
|
it 'omits deprecated tags' do
|
||||||
deprecated_tag = create(:tag,
|
deprecated_tag = Tag.create!(
|
||||||
primary_name: 'deprecated_post_tag',
|
name: 'deprecated_post_tag',
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current
|
||||||
|
)
|
||||||
PostTag.create!(post: post_record, tag: deprecated_tag)
|
PostTag.create!(post: post_record, tag: deprecated_tag)
|
||||||
|
|
||||||
request
|
request
|
||||||
@@ -859,9 +853,9 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
tags =
|
tags =
|
||||||
15.times.map do |i|
|
15.times.map do |i|
|
||||||
tag_name = create(:tag_name, name: "show_query_tag_#{ i }")
|
tag_name = TagName.create!(name: "show_query_tag_#{ i }")
|
||||||
tag = create(:tag, primary_tag_name: tag_name, category: :general)
|
tag = Tag.create!(tag_name:, category: :general)
|
||||||
create(:tag_name, :alias, name: "show_query_alias_#{ i }", tag:)
|
TagName.create!(name: "show_query_alias_#{ i }", canonical: tag_name)
|
||||||
PostTag.create!(post: post_record, tag:)
|
PostTag.create!(post: post_record, tag:)
|
||||||
tag
|
tag
|
||||||
end
|
end
|
||||||
@@ -916,9 +910,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
expect(json.fetch('tags')).to include(
|
expect(json.fetch('tags')).to include(
|
||||||
a_hash_including('id' => tag.id,
|
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'))
|
||||||
'name' => primary_tag_name_for(tag, 'ja').name,
|
|
||||||
'category' => 'general'))
|
|
||||||
external_json =
|
external_json =
|
||||||
json
|
json
|
||||||
.fetch('tags')
|
.fetch('tags')
|
||||||
@@ -1008,7 +1000,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
describe 'POST /posts' do
|
describe 'POST /posts' do
|
||||||
let(:member) { create(:user, :member) }
|
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
|
it '401 when not logged in' do
|
||||||
sign_out
|
sign_out
|
||||||
@@ -1165,10 +1157,11 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects a deprecated tag specified directly' do
|
it 'rejects a deprecated tag specified directly' do
|
||||||
create(:tag,
|
Tag.create!(
|
||||||
primary_name: 'deprecated_direct_tag',
|
name: 'deprecated_direct_tag',
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current
|
||||||
|
)
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
|
|
||||||
post '/posts', params: post_write_params(
|
post '/posts', params: post_write_params(
|
||||||
@@ -1185,16 +1178,18 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'expands through multiple deprecated parent tags and saves active ancestors' do
|
it 'expands through multiple deprecated parent tags and saves active ancestors' do
|
||||||
child = create(:tag, primary_name: 'active_child', category: :general)
|
child = Tag.create!(name: 'active_child', category: :general)
|
||||||
deprecated_parent = create(:tag,
|
deprecated_parent = Tag.create!(
|
||||||
primary_name: 'deprecated_parent',
|
name: 'deprecated_parent',
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current
|
||||||
deprecated_grandparent = create(:tag,
|
)
|
||||||
primary_name: 'deprecated_grandparent',
|
deprecated_grandparent = Tag.create!(
|
||||||
|
name: 'deprecated_grandparent',
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current
|
||||||
active_grandparent = create(:tag, primary_name: 'active_grandparent', category: :general)
|
)
|
||||||
|
active_grandparent = Tag.create!(name: 'active_grandparent', category: :general)
|
||||||
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
|
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
|
||||||
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
|
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
|
||||||
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_grandparent)
|
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_grandparent)
|
||||||
@@ -1208,8 +1203,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
saved_names = TagName.where(tag_id: Post.find(json.fetch('id')).tags.select(:id),
|
saved_names = Post.find(json.fetch('id')).tags.map(&:name)
|
||||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
|
||||||
expect(saved_names).to include('active_child', 'active_grandparent')
|
expect(saved_names).to include('active_child', 'active_grandparent')
|
||||||
expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent')
|
expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent')
|
||||||
end
|
end
|
||||||
@@ -1312,8 +1306,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
created_post = Post.find(json.fetch('id'))
|
created_post = Post.find(json.fetch('id'))
|
||||||
tag = TagName.find_by!(name: '伊地知ニジカ',
|
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
|
||||||
language_code: 'ja', primary_flg: true).tag
|
|
||||||
section = PostTagSection.find_by!(post: created_post, tag:)
|
section = PostTagSection.find_by!(post: created_post, tag:)
|
||||||
|
|
||||||
expect(section.begin_ms).to eq(60_000)
|
expect(section.begin_ms).to eq(60_000)
|
||||||
@@ -1333,8 +1326,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
created_post = Post.find(json.fetch('id'))
|
created_post = Post.find(json.fetch('id'))
|
||||||
tag = TagName.find_by!(name: '伊地知ニジカ',
|
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
|
||||||
language_code: 'ja', primary_flg: true).tag
|
|
||||||
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
|
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1351,8 +1343,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
created_post = Post.find(json.fetch('id'))
|
created_post = Post.find(json.fetch('id'))
|
||||||
tag = TagName.find_by!(name: '伊地知ニジカ',
|
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
|
||||||
language_code: 'ja', primary_flg: true).tag
|
|
||||||
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
|
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1631,7 +1622,6 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(arguments[:thumbnails].keys).to eq([0])
|
expect(arguments[:thumbnails].keys).to eq([0])
|
||||||
expect(arguments[:host]).to eq('http://www.example.com')
|
expect(arguments[:host]).to eq('http://www.example.com')
|
||||||
end
|
end
|
||||||
expect(creator).to have_received(:run).with(Locale.nipponese)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects malformed manifests as a request-level error' do
|
it 'rejects malformed manifests as a request-level error' do
|
||||||
@@ -1668,8 +1658,8 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
create(:post_tag_section, post: post_record, tag:,
|
create(:post_tag_section, post: post_record, tag:,
|
||||||
begin_ms: 1000, end_ms: 2000)
|
begin_ms: 1000, end_ms: 2000)
|
||||||
|
|
||||||
tn2 = create(:tag_name, name: 'spec_tag_2')
|
tn2 = TagName.create!(name: 'spec_tag_2')
|
||||||
replacement_tag = create(:tag, primary_tag_name: tn2, category: :general)
|
replacement_tag = Tag.create!(tag_name: tn2, category: :general)
|
||||||
|
|
||||||
put "/posts/#{post_record.id}", params: post_update_params(
|
put "/posts/#{post_record.id}", params: post_update_params(
|
||||||
post_record,
|
post_record,
|
||||||
@@ -1717,10 +1707,11 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects a deprecated tag specified directly' do
|
it 'rejects a deprecated tag specified directly' do
|
||||||
create(:tag,
|
Tag.create!(
|
||||||
primary_name: 'deprecated_update_tag',
|
name: 'deprecated_update_tag',
|
||||||
category: :general,
|
category: :general,
|
||||||
deprecated_at: Time.current)
|
deprecated_at: Time.current
|
||||||
|
)
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
|
|
||||||
put "/posts/#{ post_record.id }", params: post_update_params(
|
put "/posts/#{ post_record.id }", params: post_update_params(
|
||||||
@@ -1975,7 +1966,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
base_version_no: base_version.version_no,
|
base_version_no: base_version.version_no,
|
||||||
title: 'updated by me',
|
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)
|
expect(response).to have_http_status(:conflict)
|
||||||
|
|
||||||
@@ -1995,8 +1986,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
base_version = create_post_version_for!(post_record.reload)
|
base_version = create_post_version_for!(post_record.reload)
|
||||||
|
|
||||||
current_tag = Tag.find_or_create_by_tag_name!(
|
current_tag = Tag.find_or_create_by_tag_name!('current_added_tag', category: :general)
|
||||||
Locale.nipponese, 'current_added_tag', category: :general)
|
|
||||||
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
||||||
|
|
||||||
PostVersionRecorder.record!(
|
PostVersionRecorder.record!(
|
||||||
@@ -2007,8 +1997,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
base_version_no: base_version.version_no,
|
base_version_no: base_version.version_no,
|
||||||
title: post_record.title,
|
title: post_record.title,
|
||||||
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
|
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_added_tag")
|
||||||
'incoming_added_tag'].join(' '))
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:conflict)
|
expect(response).to have_http_status(:conflict)
|
||||||
|
|
||||||
@@ -2026,8 +2015,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
base_version = create_post_version_for!(post_record.reload)
|
base_version = create_post_version_for!(post_record.reload)
|
||||||
|
|
||||||
current_tag = Tag.find_or_create_by_tag_name!(
|
current_tag = Tag.find_or_create_by_tag_name!('current_merge_tag', category: :general)
|
||||||
Locale.nipponese, 'current_merge_tag', category: :general)
|
|
||||||
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
||||||
|
|
||||||
PostVersionRecorder.record!(
|
PostVersionRecorder.record!(
|
||||||
@@ -2038,17 +2026,15 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
base_version_no: base_version.version_no,
|
base_version_no: base_version.version_no,
|
||||||
title: post_record.title,
|
title: post_record.title,
|
||||||
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
|
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_merge_tag",
|
||||||
'incoming_merge_tag'].join(' '),
|
|
||||||
merge: '1')
|
merge: '1')
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
names = post_record.reload.tags.map(&:name)
|
||||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
|
||||||
|
|
||||||
expect(names).to include('spec_tag')
|
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('current_merge_tag')
|
||||||
expect(names).to include('incoming_merge_tag')
|
expect(names).to include('incoming_merge_tag')
|
||||||
end
|
end
|
||||||
@@ -2071,15 +2057,14 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
base_version_no: base_version.version_no,
|
base_version_no: base_version.version_no,
|
||||||
title: post_record.title,
|
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)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
names = post_record.reload.tags.map(&:name)
|
||||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
|
||||||
|
|
||||||
expect(names).to include('spec_tag')
|
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(post_record.external_tags).to contain_exactly(nico_tag)
|
expect(post_record.external_tags).to contain_exactly(nico_tag)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2094,15 +2079,14 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
base_version_no: base_version.version_no,
|
base_version_no: base_version.version_no,
|
||||||
title: 'updated title',
|
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)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
names = post_record.reload.tags.map(&:name)
|
||||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
|
||||||
|
|
||||||
expect(names).to include('spec_tag')
|
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(post_record.external_tags).to contain_exactly(nico_tag)
|
expect(post_record.external_tags).to contain_exactly(nico_tag)
|
||||||
|
|
||||||
external_json =
|
external_json =
|
||||||
@@ -2118,8 +2102,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
|
|
||||||
nico_tag = create_nico_tag!('nico:relation_source')
|
nico_tag = create_nico_tag!('nico:relation_source')
|
||||||
linked_tag = Tag.find_or_create_by_tag_name!(
|
linked_tag = Tag.find_or_create_by_tag_name!('relation_linked_tag', category: :general)
|
||||||
Locale.nipponese, 'relation_linked_tag', category: :general)
|
|
||||||
|
|
||||||
NicoTagRelation.create!(nico_tag:, tag: linked_tag)
|
NicoTagRelation.create!(nico_tag:, tag: linked_tag)
|
||||||
PostExternalTag.create!(post: post_record, external_tag: nico_tag)
|
PostExternalTag.create!(post: post_record, external_tag: nico_tag)
|
||||||
@@ -2130,17 +2113,16 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
base_version_no: base_version.version_no,
|
base_version_no: base_version.version_no,
|
||||||
title: post_record.title,
|
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)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
|
names = post_record.reload.tags.map(&:name)
|
||||||
language_code: 'ja', primary_flg: true).pluck(:name)
|
|
||||||
|
|
||||||
expect(post_record.external_tags).to contain_exactly(nico_tag)
|
expect(post_record.external_tags).to contain_exactly(nico_tag)
|
||||||
expect(names).to include('spec_tag')
|
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).not_to include(primary_tag_name_for(linked_tag, 'ja').name)
|
expect(names).not_to include(linked_tag.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'force-updates stale posts without base_version_no' do
|
it 'force-updates stale posts without base_version_no' do
|
||||||
@@ -2156,7 +2138,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
title: 'forced title',
|
title: 'forced title',
|
||||||
tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}",
|
tags: "spec_tag #{Tag.no_deerjikist.name}",
|
||||||
force: '1')
|
force: '1')
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
@@ -2209,8 +2191,8 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
let(:oc_from) { Time.zone.local(2019, 12, 31, 0, 0, 0) }
|
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(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) }
|
||||||
|
|
||||||
let!(:tag_name2) { create(:tag_name, name: 'spec_tag_2') }
|
let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') }
|
||||||
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :general) }
|
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) }
|
||||||
|
|
||||||
def snapshot_tags(post)
|
def snapshot_tags(post)
|
||||||
post.snapshot_tag_names.join(' ')
|
post.snapshot_tag_names.join(' ')
|
||||||
@@ -2435,7 +2417,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json.fetch('count')).to eq(3)
|
expect(json.fetch('count')).to eq(3)
|
||||||
expect(json.fetch('versions').first.fetch('tags')).to include(
|
expect(json.fetch('versions').first.fetch('tags')).to include(
|
||||||
'name' => primary_tag_name_for(tag2, 'ja').name, 'type' => 'context')
|
'name' => tag2.name, 'type' => 'context')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters versions by tag when the current snapshot includes the tag' do
|
it 'filters versions by tag when the current snapshot includes the tag' do
|
||||||
@@ -2568,8 +2550,8 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
base_version = create_post_version_for!(post_record)
|
base_version = create_post_version_for!(post_record)
|
||||||
|
|
||||||
tag_name2 = create(:tag_name, name: 'spec_tag_2')
|
tag_name2 = TagName.create!(name: 'spec_tag_2')
|
||||||
create(:tag, primary_tag_name: tag_name2, category: :general)
|
Tag.create!(tag_name: tag_name2, category: :general)
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
@@ -2733,8 +2715,8 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
base_version = create_post_version_for!(post_record.reload)
|
base_version = create_post_version_for!(post_record.reload)
|
||||||
|
|
||||||
tag_name2 = create(:tag_name, name: 'spec_tag_2')
|
tag_name2 = TagName.create!(name: 'spec_tag_2')
|
||||||
tag2 = create(:tag, primary_tag_name: tag_name2, category: :general)
|
tag2 = Tag.create!(tag_name: tag_name2, category: :general)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
put "/posts/#{post_record.id}", params: post_write_params(
|
put "/posts/#{post_record.id}", params: post_write_params(
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag description Wiki compatibility', type: :request do
|
|
||||||
let!(:user) { create_member_user! }
|
|
||||||
let!(:tag_name) { create(:tag_name, name: 'description_contract') }
|
|
||||||
let!(:page) do
|
|
||||||
Wiki::Commit.create_content!(
|
|
||||||
tag_name:, body: 'old-revision', created_by_user: user)
|
|
||||||
end
|
|
||||||
let!(:revisions) do
|
|
||||||
first = page.current_revision
|
|
||||||
second = Wiki::Commit.content!(page:, body: 'old-second', created_user: user)
|
|
||||||
third = Wiki::Commit.content!(page:, body: 'old-third', created_user: user)
|
|
||||||
[first, second, third]
|
|
||||||
end
|
|
||||||
let!(:tag) { create(:tag, primary_tag_name: tag_name) }
|
|
||||||
let!(:versions) do
|
|
||||||
revisions.each_with_index.map do |revision, index|
|
|
||||||
TagDescriptionVersion.create!(
|
|
||||||
id: revision.id, tag_id: tag.id, language_code: 'ja',
|
|
||||||
version_no: index + 1, event_type: index.zero? ? 'create' : 'update',
|
|
||||||
body: index == 2 ? 'new-version' : 'A',
|
|
||||||
created_by_user_id: user.id, created_at: revision.created_at)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
let!(:description) do
|
|
||||||
TagDescription.create!(
|
|
||||||
id: page.id, tag:, language_code: 'ja', version_no: 3, body: 'new',
|
|
||||||
created_by_user_id: user.id, updated_by_user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
before { page.update_columns(body: 'old') }
|
|
||||||
|
|
||||||
# Observe real SELECTs, including cached queries, only during the request.
|
|
||||||
def without_legacy_reads
|
|
||||||
queries = []
|
|
||||||
subscriber = lambda do |*args|
|
|
||||||
sql = args.last.fetch(:sql)
|
|
||||||
if sql.match?(/\bSELECT\b/i) && sql.match?(/\bwiki_(pages|revisions|versions)\b/i)
|
|
||||||
queries << sql
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') { yield }
|
|
||||||
expect(queries).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'show' do
|
|
||||||
it 'uses current content and revision identity without legacy reads' do
|
|
||||||
without_legacy_reads { get "/wiki/#{ description.id }" }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('body' => 'new', 'revision_id' => versions.last.id,
|
|
||||||
'pred' => versions.second.id, 'succ' => nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
[0, 1, 2].each do |index|
|
|
||||||
it "navigates displayed version #{ index + 1 }, preserving repeated bodies" do
|
|
||||||
without_legacy_reads do
|
|
||||||
get "/wiki/#{ description.id }", params: { version: versions[index].id }
|
|
||||||
end
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include(
|
|
||||||
'body' => versions[index].body,
|
|
||||||
'revision_id' => versions[index].id,
|
|
||||||
'pred' => index.zero? ? nil : versions[index - 1].id,
|
|
||||||
'succ' => index == 2 ? nil : versions[index + 1].id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not fall back to a revision absent from new history' do
|
|
||||||
versions.first.destroy!
|
|
||||||
without_legacy_reads do
|
|
||||||
get "/wiki/#{ description.id }", params: { version: revisions.first.id }
|
|
||||||
end
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not fall back when the new current revision is missing' do
|
|
||||||
versions.last.destroy!
|
|
||||||
without_legacy_reads { get "/wiki/#{ description.id }" }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'revision resource scope' do
|
|
||||||
let!(:other_tag) { create(:tag, primary_name: 'other_description_contract') }
|
|
||||||
let!(:other_version) do
|
|
||||||
TagDescriptionVersion.create!(
|
|
||||||
tag_id: other_tag.id, language_code: 'ja', version_no: 1,
|
|
||||||
event_type: 'create', body: 'other tag history',
|
|
||||||
created_by_user_id: user.id, created_at: Time.current)
|
|
||||||
end
|
|
||||||
let!(:other_description) do
|
|
||||||
TagDescription.create!(
|
|
||||||
tag: other_tag, language_code: 'ja', version_no: 1, body: 'other tag history',
|
|
||||||
created_by_user_id: user.id, updated_by_user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 404 when show specifies another tag description version' do
|
|
||||||
get "/wiki/#{ description.id }", params: { version: other_version.id }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
expect(response.body).not_to include(other_version.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 404 when diff from belongs to another tag description' do
|
|
||||||
get "/wiki/#{ description.id }/diff",
|
|
||||||
params: { from: other_version.id, to: versions.last.id }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
expect(response.body).not_to include(other_version.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 404 when diff to belongs to another tag description' do
|
|
||||||
get "/wiki/#{ description.id }/diff",
|
|
||||||
params: { from: versions.first.id, to: other_version.id }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
expect(response.body).not_to include(other_version.body)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'show_by_title' do
|
|
||||||
it 'resolves TagName and reads current content from new tables' do
|
|
||||||
without_legacy_reads { get "/wiki/title/#{ tag_name.name }" }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('id' => description.id, 'body' => 'new',
|
|
||||||
'revision_id' => versions.last.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'uses historical content and navigation from the new history' do
|
|
||||||
get "/wiki/title/#{ tag_name.name }", params: { version: versions.second.id }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('body' => 'A', 'revision_id' => versions.second.id,
|
|
||||||
'pred' => versions.first.id, 'succ' => versions.last.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'ignores changes to same-title legacy content and revision metadata' do
|
|
||||||
get "/wiki/title/#{ tag_name.name }"
|
|
||||||
original = json
|
|
||||||
page.update_columns(body: 'changed legacy body')
|
|
||||||
revisions.last.update_columns(message: 'legacy message', created_at: 1.day.ago)
|
|
||||||
revisions.last.wiki_lines.update_all(body: 'changed legacy revision')
|
|
||||||
get "/wiki/title/#{ tag_name.name }"
|
|
||||||
expect(json).to eq(original)
|
|
||||||
expect(json.fetch('body')).to eq('new')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'exists' do
|
|
||||||
it 'recognises an ID without reading legacy tables' do
|
|
||||||
without_legacy_reads { get "/wiki/#{ description.id }/exists" }
|
|
||||||
expect(response).to have_http_status(:no_content)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'recognises a title without reading legacy tables' do
|
|
||||||
without_legacy_reads { get "/wiki/title/#{ tag_name.name }/exists" }
|
|
||||||
expect(response).to have_http_status(:no_content)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'without a legacy page' do
|
|
||||||
before do
|
|
||||||
WikiVersion.where(wiki_page_id: page.id).delete_all
|
|
||||||
page.destroy!
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'recognises the description ID alone' do
|
|
||||||
get "/wiki/#{ description.id }/exists"
|
|
||||||
expect(response).to have_http_status(:no_content)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'recognises the TagName alone' do
|
|
||||||
get "/wiki/title/#{ tag_name.name }/exists"
|
|
||||||
expect(response).to have_http_status(:no_content)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows the description by title' do
|
|
||||||
get "/wiki/title/#{ tag_name.name }"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('body' => 'new', 'revision_id' => versions.last.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'diff' do
|
|
||||||
[false, true].each do |current|
|
|
||||||
it "uses new bodies with #{ current ? 'implicit current' : 'explicit to' }" do
|
|
||||||
params = { from: versions.first.id }
|
|
||||||
params[:to] = versions.last.id unless current
|
|
||||||
get "/wiki/#{ description.id }/diff", params: params
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include(
|
|
||||||
'wiki_page_id' => description.id,
|
|
||||||
'older_revision_id' => versions.first.id,
|
|
||||||
'newer_revision_id' => versions.last.id,
|
|
||||||
'diff' => [{ 'type' => 'removed', 'content' => 'A' },
|
|
||||||
{ 'type' => 'added', 'content' => 'new-version' }])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not query legacy tables for diff' do
|
|
||||||
without_legacy_reads do
|
|
||||||
get "/wiki/#{ description.id }/diff",
|
|
||||||
params: { from: versions.first.id, to: versions.last.id }
|
|
||||||
end
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'keeps distinct revision IDs for identical consecutive bodies' do
|
|
||||||
get "/wiki/#{ description.id }/diff",
|
|
||||||
params: { from: versions.first.id, to: versions.second.id }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include(
|
|
||||||
'older_revision_id' => versions.first.id,
|
|
||||||
'newer_revision_id' => versions.second.id,
|
|
||||||
'diff' => [{ 'type' => 'context', 'content' => 'A' }])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'changes' do
|
|
||||||
it 'lists every new version including consecutive identical bodies' do
|
|
||||||
without_legacy_reads { get '/wiki/changes', params: { id: description.id } }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
|
|
||||||
expect(json.first.keys).to match_array(
|
|
||||||
['revision_id', 'pred', 'succ', 'wiki_page', 'user', 'kind', 'message', 'timestamp'])
|
|
||||||
expect(json.first).to include(
|
|
||||||
'pred' => versions.second.id, 'succ' => nil, 'kind' => 'content',
|
|
||||||
'timestamp' => versions.last.created_at.iso8601(3),
|
|
||||||
'user' => { 'id' => user.id, 'name' => user.name })
|
|
||||||
expect(json.first.fetch('wiki_page')).to include('id' => description.id,
|
|
||||||
'title' => tag_name.name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'ignores legacy-only revisions and metadata changes' do
|
|
||||||
get '/wiki/changes', params: { id: description.id }
|
|
||||||
original = json
|
|
||||||
revisions.first.update_columns(message: 'legacy changed', created_at: 1.day.ago)
|
|
||||||
WikiRevision.create!(
|
|
||||||
wiki_page: page, created_user: user, kind: :content,
|
|
||||||
tree_sha256: Digest::SHA256.hexdigest('legacy only'), lines_count: 0)
|
|
||||||
get '/wiki/changes', params: { id: description.id }
|
|
||||||
expect(json).to eq(original)
|
|
||||||
expect(json.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'mixed changes' do
|
|
||||||
it 'includes unmigrated history and ignores migrated legacy-only revisions' do
|
|
||||||
legacy_page = Wiki::Commit.create_content!(
|
|
||||||
tag_name: create(:tag_name, name: 'Help_mixed'),
|
|
||||||
body: 'help', created_by_user: user)
|
|
||||||
legacy_revision = legacy_page.current_revision
|
|
||||||
extra = WikiRevision.create!(
|
|
||||||
wiki_page: page, created_user: user, kind: :content,
|
|
||||||
tree_sha256: Digest::SHA256.hexdigest('legacy only'), lines_count: 0)
|
|
||||||
|
|
||||||
get '/wiki/changes'
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
migrated = json.select { |row| row.fetch('wiki_page').fetch('id') == description.id }
|
|
||||||
expect(migrated.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
|
|
||||||
expect(json.map { |row| row.fetch('revision_id') }).not_to include(extra.id)
|
|
||||||
expect(json).to include(include(
|
|
||||||
'revision_id' => legacy_revision.id,
|
|
||||||
'wiki_page' => include('id' => legacy_page.id, 'title' => 'Help_mixed')))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'index' do
|
|
||||||
[nil, 'description_contract'].each do |title|
|
|
||||||
it "returns new content once in #{ title ? 'filtered' : 'unfiltered' } index" do
|
|
||||||
get '/wiki', params: { title: }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
rows = json.select { |row| row.fetch('id') == description.id }
|
|
||||||
expect(rows.size).to eq(1)
|
|
||||||
expect(rows.first).to include('body' => 'new', 'title' => tag_name.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag description Wiki dual writes', type: :request do
|
|
||||||
let!(:user) { create_member_user! }
|
|
||||||
let!(:tag_name) { create(:tag_name, name: 'description_write') }
|
|
||||||
let(:tag) { create(:tag, primary_tag_name: tag_name) }
|
|
||||||
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
|
|
||||||
|
|
||||||
def stored_state
|
|
||||||
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion,
|
|
||||||
TagName, TagVersion].map do |model|
|
|
||||||
model.order(:id).map(&:attributes)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Fail after a real INSERT/UPDATE, exercising rollback rather than a mock write.
|
|
||||||
def fail_save model
|
|
||||||
failure = Class.new(StandardError)
|
|
||||||
callback = ->(_record) { raise failure, 'injected persistence failure' }
|
|
||||||
model.set_callback(:save, :after, callback)
|
|
||||||
expect { yield }.to raise_error(failure)
|
|
||||||
ensure
|
|
||||||
model.skip_callback(:save, :after, callback)
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST /wiki' do
|
|
||||||
before { tag }
|
|
||||||
|
|
||||||
def create_description
|
|
||||||
post '/wiki', params: { title: tag_name.name, body: 'initial', message: 'create' },
|
|
||||||
headers: headers
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates both current records and histories with a shared revision ID' do
|
|
||||||
expect { create_description }
|
|
||||||
.to change(WikiPage, :count).by(1)
|
|
||||||
.and change(WikiRevision, :count).by(1)
|
|
||||||
.and change(WikiVersion, :count).by(1)
|
|
||||||
.and change(TagDescription, :count).by(1)
|
|
||||||
.and change(TagDescriptionVersion, :count).by(1)
|
|
||||||
expect(response).to have_http_status(:created)
|
|
||||||
|
|
||||||
page = WikiPage.find(json.fetch('id'))
|
|
||||||
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
|
||||||
version = TagDescriptionVersion.find_by!(tag_id: tag.id, language_code: 'ja')
|
|
||||||
expect(description).to have_attributes(id: page.id, body: 'initial', version_no: 1)
|
|
||||||
expect(version).to have_attributes(
|
|
||||||
id: page.current_revision.id, body: 'initial', version_no: 1,
|
|
||||||
event_type: 'create', created_by_user_id: user.id)
|
|
||||||
expect(page.body).to eq(description.body)
|
|
||||||
expect(page.current_revision.body).to eq(version.body)
|
|
||||||
expect(page.wiki_versions.last.body).to eq(version.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
['missing_description', 'orphan_description'].each do |title|
|
|
||||||
it "rejects the bare title #{ title } without writing any resource" do
|
|
||||||
create(:tag_name, name: title) if title == 'orphan_description'
|
|
||||||
before_state = stored_state
|
|
||||||
|
|
||||||
post '/wiki', params: { title:, body: 'initial' }, headers: headers
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
|
||||||
expect(json).to include('type' => 'validation_error')
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a namespaced legacy Wiki resource without a Tag' do
|
|
||||||
expect do
|
|
||||||
post '/wiki', params: { title: 'Help:description_write', body: 'legacy' },
|
|
||||||
headers: headers
|
|
||||||
end.to change(WikiPage, :count).by(1)
|
|
||||||
.and change(WikiRevision, :count).by(1)
|
|
||||||
.and change(WikiVersion, :count).by(1)
|
|
||||||
.and change(TagDescription, :count).by(0)
|
|
||||||
.and change(TagDescriptionVersion, :count).by(0)
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
|
||||||
page = WikiPage.find(json.fetch('id'))
|
|
||||||
expect(page.tag_name.tag).to be_nil
|
|
||||||
expect(page.body).to eq('legacy')
|
|
||||||
expect(page.current_revision.body).to eq('legacy')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates only legacy Wiki for a namespaced title with an existing Tag' do
|
|
||||||
namespaced_name = create(:tag_name, name: 'help:foo')
|
|
||||||
namespaced_tag = create(:tag, primary_tag_name: namespaced_name)
|
|
||||||
|
|
||||||
expect do
|
|
||||||
post '/wiki', params: { title: namespaced_name.name, body: 'legacy' },
|
|
||||||
headers: headers
|
|
||||||
end.to change(WikiPage, :count).by(1)
|
|
||||||
.and change(WikiRevision, :count).by(1)
|
|
||||||
.and change(WikiVersion, :count).by(1)
|
|
||||||
.and change(TagName, :count).by(0)
|
|
||||||
.and change(TagDescription, :count).by(0)
|
|
||||||
.and change(TagDescriptionVersion, :count).by(0)
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
|
||||||
page = WikiPage.find(json.fetch('id'))
|
|
||||||
expect(page).to have_attributes(tag_name_id: namespaced_name.id, body: 'legacy')
|
|
||||||
expect(page.tag_name.tag).to eq(namespaced_tag)
|
|
||||||
expect(page.current_revision.body).to eq('legacy')
|
|
||||||
expect(page.wiki_versions.last.body).to eq('legacy')
|
|
||||||
end
|
|
||||||
|
|
||||||
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
|
||||||
it "rolls back both systems when #{ model.name } persistence fails" do
|
|
||||||
tag_name
|
|
||||||
before_state = stored_state
|
|
||||||
aggregate_failures do
|
|
||||||
fail_save(model) { create_description }
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'PUT /wiki/:id' do
|
|
||||||
let!(:page) do
|
|
||||||
tag
|
|
||||||
Wiki::Commit.create_content!(
|
|
||||||
tag_name:, body: 'before', created_by_user: user)
|
|
||||||
end
|
|
||||||
let!(:first_revision) { page.current_revision }
|
|
||||||
|
|
||||||
let!(:description) do
|
|
||||||
TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_description body: 'after', title: tag_name.name, base: first_revision.id
|
|
||||||
put "/wiki/#{ page.id }",
|
|
||||||
params: { title:, body:, message: 'edit', base_revision_id: base },
|
|
||||||
headers: headers
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'increments the independent description sequence and shares revision identity' do
|
|
||||||
expect(page.reload.version_no).to eq(1)
|
|
||||||
expect(description.version_no).to eq(1)
|
|
||||||
expect { update_description }
|
|
||||||
.to change(WikiRevision, :count).by(1)
|
|
||||||
.and change(WikiVersion, :count).by(1)
|
|
||||||
.and change(TagDescriptionVersion, :count).by(1)
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
|
|
||||||
revision = page.reload.current_revision
|
|
||||||
version = TagDescriptionVersion.find(revision.id)
|
|
||||||
expect(description.reload).to have_attributes(body: 'after', version_no: 2)
|
|
||||||
expect(version).to have_attributes(
|
|
||||||
body: 'after', version_no: 2, event_type: 'update', created_by_user_id: user.id)
|
|
||||||
expect(page).to have_attributes(body: 'after', version_no: 2)
|
|
||||||
expect(revision.body).to eq(version.body)
|
|
||||||
expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'uses description identity even when its Wiki TagName no longer owns the Tag' do
|
|
||||||
tag_name.update_columns(tag_id: nil)
|
|
||||||
|
|
||||||
expect { update_description }
|
|
||||||
.to change(WikiRevision, :count).by(1)
|
|
||||||
.and change(WikiVersion, :count).by(1)
|
|
||||||
.and change(TagDescriptionVersion, :count).by(1)
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(description.reload).to have_attributes(body: 'after', version_no: 2)
|
|
||||||
expect(TagDescriptionVersion.find(page.reload.current_revision.id))
|
|
||||||
.to have_attributes(tag_id: tag.id, body: 'after', version_no: 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not add body history for a title-only update' do
|
|
||||||
revision_count = WikiRevision.count
|
|
||||||
wiki_version_count = WikiVersion.count
|
|
||||||
description_version_count = TagDescriptionVersion.count
|
|
||||||
wiki_version_no = page.reload.version_no
|
|
||||||
description_version_no = description.version_no
|
|
||||||
current_revision_id = page.current_revision.id
|
|
||||||
page_state = page.attributes
|
|
||||||
description_state = description.attributes
|
|
||||||
|
|
||||||
update_description(body: 'before', title: 'Help:description_renamed')
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(tag_name.reload.name).to eq('Help:description_renamed')
|
|
||||||
expect(WikiRevision.count).to eq(revision_count)
|
|
||||||
expect(WikiVersion.count).to eq(wiki_version_count)
|
|
||||||
expect(TagDescriptionVersion.count).to eq(description_version_count)
|
|
||||||
expect(page.reload).to have_attributes(body: 'before', version_no: wiki_version_no)
|
|
||||||
expect(page.current_revision.id).to eq(current_revision_id)
|
|
||||||
expect(page.reload.attributes).to eq(page_state)
|
|
||||||
expect(description.reload).to have_attributes(
|
|
||||||
body: 'before', version_no: description_version_no)
|
|
||||||
expect(description.reload.attributes).to eq(description_state)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not add body history for a TagName rename through tags' do
|
|
||||||
revision_count = WikiRevision.count
|
|
||||||
wiki_version_count = WikiVersion.count
|
|
||||||
description_version_count = TagDescriptionVersion.count
|
|
||||||
page_version_no = page.reload.version_no
|
|
||||||
current_revision_id = page.current_revision.id
|
|
||||||
|
|
||||||
patch "/tags/#{ tag.id }",
|
|
||||||
params: { name: 'description_tag_renamed' },
|
|
||||||
headers: headers
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(tag_name.reload.name).to eq('description_tag_renamed')
|
|
||||||
expect(WikiRevision.count).to eq(revision_count)
|
|
||||||
expect(WikiVersion.count).to eq(wiki_version_count)
|
|
||||||
expect(TagDescriptionVersion.count).to eq(description_version_count)
|
|
||||||
expect(page.reload.version_no).to eq(page_version_no)
|
|
||||||
expect(page.current_revision.id).to eq(current_revision_id)
|
|
||||||
expect(description.reload).to have_attributes(
|
|
||||||
body: 'before', version_no: 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'keeps both systems and the title unchanged on a stale revision conflict' do
|
|
||||||
update_description(body: 'current body', base: first_revision.id)
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
|
|
||||||
stale_revision_id = first_revision.id
|
|
||||||
before_state = stored_state
|
|
||||||
|
|
||||||
update_description(
|
|
||||||
body: 'conflicting body',
|
|
||||||
title: 'conflicting_rename',
|
|
||||||
base: stale_revision_id)
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:conflict)
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
expect(tag_name.reload.name).not_to eq('conflicting_rename')
|
|
||||||
end
|
|
||||||
|
|
||||||
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
|
||||||
it "rolls back both systems when #{ model.name } persistence fails" do
|
|
||||||
before_state = stored_state
|
|
||||||
aggregate_failures do
|
|
||||||
fail_save(model) do
|
|
||||||
update_description(body: 'rollback body', title: 'rollback_rename')
|
|
||||||
end
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[['identical body', 'before'],
|
|
||||||
['normalised-equivalent body', "before\n\n"]].each do |label, body|
|
|
||||||
it "does not write histories for #{ label }" do
|
|
||||||
before_state = stored_state
|
|
||||||
page_state = page.reload.attributes
|
|
||||||
description_state = description.reload.attributes
|
|
||||||
current_revision_id = page.current_revision.id
|
|
||||||
wiki_lines = WikiLine.order(:id).map(&:attributes)
|
|
||||||
revision_lines = WikiRevisionLine.order(:id).map(&:attributes)
|
|
||||||
page_version_no = page.version_no
|
|
||||||
description_version_no = description.version_no
|
|
||||||
|
|
||||||
update_description(body:)
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(WikiRevision.count).to eq(before_state[1].length)
|
|
||||||
expect(WikiVersion.count).to eq(before_state[2].length)
|
|
||||||
expect(TagDescriptionVersion.count).to eq(before_state[4].length)
|
|
||||||
expect(page.reload.version_no).to eq(page_version_no)
|
|
||||||
expect(description.reload.version_no).to eq(description_version_no)
|
|
||||||
expect(page.current_revision.id).to eq(current_revision_id)
|
|
||||||
expect(page.reload.attributes).to eq(page_state)
|
|
||||||
expect(description.reload.attributes).to eq(description_state)
|
|
||||||
expect(WikiLine.order(:id).map(&:attributes)).to eq(wiki_lines)
|
|
||||||
expect(WikiRevisionLine.order(:id).map(&:attributes)).to eq(revision_lines)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag description native API', type: :request do
|
|
||||||
let!(:author) { create_member_user! }
|
|
||||||
let(:editor) { create(:user, :member, name: 'description editor') }
|
|
||||||
let(:tag_name) { create(:tag_name, name: 'native_description') }
|
|
||||||
let(:tag) { create(:tag, primary_tag_name: tag_name) }
|
|
||||||
let(:headers) { { 'X-Transfer-Code' => editor.inheritance_code } }
|
|
||||||
let(:endpoint) { "/tags/#{ tag.id }/description" }
|
|
||||||
|
|
||||||
def stored_state
|
|
||||||
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion,
|
|
||||||
WikiLine, WikiRevisionLine, TagName, TagVersion].map do |model|
|
|
||||||
model.order(:id).map(&:attributes)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fail_save model
|
|
||||||
failure = Class.new(StandardError)
|
|
||||||
callback = ->(_record) { raise failure, 'injected persistence failure' }
|
|
||||||
model.set_callback(:save, :after, callback)
|
|
||||||
expect { yield }.to raise_error(failure)
|
|
||||||
ensure
|
|
||||||
model.skip_callback(:save, :after, callback)
|
|
||||||
end
|
|
||||||
|
|
||||||
def without_legacy_reads
|
|
||||||
queries = []
|
|
||||||
subscriber = lambda do |*args|
|
|
||||||
sql = args.last.fetch(:sql)
|
|
||||||
if sql.match?(/\bSELECT\b/i) && sql.match?(/\bwiki_(pages|revisions|versions)\b/i)
|
|
||||||
queries << sql
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') { yield }
|
|
||||||
expect(queries).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
def native_get path, action: 'show'
|
|
||||||
get path
|
|
||||||
# A missing route must not make the resource-not-found examples pass.
|
|
||||||
expect(request.path_parameters).to include(controller: 'tag_descriptions', action:)
|
|
||||||
end
|
|
||||||
|
|
||||||
def native_write body:, request_headers: headers
|
|
||||||
put endpoint, params: { body: }, headers: request_headers
|
|
||||||
expect(request.path_parameters).to include(
|
|
||||||
controller: 'tag_descriptions', action: 'update')
|
|
||||||
end
|
|
||||||
|
|
||||||
def expect_current description
|
|
||||||
expect(json).to include(
|
|
||||||
'tag_id' => description.tag_id,
|
|
||||||
'language_code' => 'ja', 'body' => description.body,
|
|
||||||
'version_no' => description.version_no,
|
|
||||||
'created_by_user_id' => description.created_by_user_id,
|
|
||||||
'updated_by_user_id' => description.updated_by_user_id,
|
|
||||||
'created_at' => description.created_at.iso8601(3),
|
|
||||||
'updated_at' => description.updated_at.iso8601(3))
|
|
||||||
expect(json.keys).not_to include('title', 'name', 'wiki_page', 'tag_name_id')
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'routing', type: :routing do
|
|
||||||
it 'routes GET by Tag ID to show' do
|
|
||||||
expect(get: '/tags/123/description').to route_to(
|
|
||||||
controller: 'tag_descriptions', action: 'show', id: '123')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'routes GET by TagName to show_by_title' do
|
|
||||||
expect(get: '/tags/name/native_description/description').to route_to(
|
|
||||||
controller: 'tag_descriptions', action: 'show_by_title', name: 'native_description')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not route POST by Tag ID' do
|
|
||||||
expect(post: '/tags/123/description').not_to be_routable
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'routes PUT by Tag ID to update' do
|
|
||||||
expect(put: '/tags/123/description').to route_to(
|
|
||||||
controller: 'tag_descriptions', action: 'update', id: '123')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'native reads' do
|
|
||||||
let!(:description) do
|
|
||||||
TagDescription.create!(
|
|
||||||
id: tag.id + 1_000_000, tag:, language_code: 'ja', body: 'native current',
|
|
||||||
version_no: 1, created_by_user_id: author.id, updated_by_user_id: editor.id)
|
|
||||||
end
|
|
||||||
let!(:version) do
|
|
||||||
TagDescriptionVersion.create!(
|
|
||||||
tag_id: tag.id, language_code: 'ja', body: 'native version', version_no: 1,
|
|
||||||
event_type: 'create', created_by_user_id: author.id, created_at: Time.current)
|
|
||||||
end
|
|
||||||
let(:name_endpoint) { "/tags/name/#{ tag_name.name }/description" }
|
|
||||||
|
|
||||||
it 'reads the current resource by Tag ID without a WikiPage' do
|
|
||||||
expect(description.id).not_to eq(tag.id)
|
|
||||||
expect(WikiPage.exists?(id: description.id)).to be(false)
|
|
||||||
without_legacy_reads { native_get(endpoint) }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect_current(description)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not interpret a compatibility handle as a Tag ID' do
|
|
||||||
other_tag = create(:tag, id: description.id, primary_name: 'handle_collision')
|
|
||||||
other = TagDescription.create!(
|
|
||||||
tag: other_tag, language_code: 'ja', body: 'other current', version_no: 1,
|
|
||||||
created_by_user_id: author.id, updated_by_user_id: author.id)
|
|
||||||
without_legacy_reads { native_get("/tags/#{ other_tag.id }/description") }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect_current(other)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with another language for the same Tag' do
|
|
||||||
before do
|
|
||||||
# Seed only the reference row; V1 does not enable an English locale.
|
|
||||||
Language.connection.execute(<<~SQL)
|
|
||||||
INSERT IGNORE INTO languages (code, name, created_at)
|
|
||||||
VALUES ('en', 'English', CURRENT_TIMESTAMP)
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'selects the Japanese resource' do
|
|
||||||
TagDescription.create!(
|
|
||||||
tag:, language_code: 'en', body: 'English current', version_no: 4,
|
|
||||||
created_by_user_id: author.id, updated_by_user_id: author.id)
|
|
||||||
without_legacy_reads { native_get(endpoint) }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect_current(description)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 404 if only the English description exists' do
|
|
||||||
description.update!(language_code: 'en')
|
|
||||||
without_legacy_reads { native_get(endpoint) }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resolves the primary Japanese TagName without a WikiPage' do
|
|
||||||
without_legacy_reads { native_get(name_endpoint, action: 'show_by_title') }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect_current(description)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resolves an alias through the existing TagName canonicalisation semantics' do
|
|
||||||
alias_name = create(:tag_name, :alias, tag:, name: 'native_alias')
|
|
||||||
expect(TagName.canonicalise(Locale.nipponese, [alias_name.name])).to eq([tag_name.name])
|
|
||||||
without_legacy_reads do
|
|
||||||
native_get("/tags/name/#{ alias_name.name }/description", action: 'show_by_title')
|
|
||||||
end
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect_current(description)
|
|
||||||
end
|
|
||||||
|
|
||||||
[:id, :name].each do |lookup|
|
|
||||||
it "ignores divergent legacy body and history for #{ lookup } lookup" do
|
|
||||||
page = WikiPage.create!(
|
|
||||||
id: description.id, tag_name:, body: 'old current',
|
|
||||||
created_user: author, updated_user: author)
|
|
||||||
revision = WikiRevision.create!(
|
|
||||||
wiki_page: page, created_user: author, kind: :content,
|
|
||||||
lines_count: 1, tree_sha256: Digest::SHA256.hexdigest('old revision'))
|
|
||||||
line = WikiLine.create!(body: 'old revision', sha256: revision.tree_sha256)
|
|
||||||
WikiRevisionLine.create!(wiki_revision: revision, wiki_line: line, position: 0)
|
|
||||||
WikiVersionRecorder.record!(page:, event_type: :create, created_by_user: author)
|
|
||||||
path = lookup == :id ? endpoint : name_endpoint
|
|
||||||
action = lookup == :id ? 'show' : 'show_by_title'
|
|
||||||
|
|
||||||
without_legacy_reads { native_get(path, action:) }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect_current(description)
|
|
||||||
original = json
|
|
||||||
page.update_columns(body: 'changed old current')
|
|
||||||
line.update_columns(body: 'changed old revision')
|
|
||||||
revision.update_columns(message: 'changed old history', created_at: 1.day.ago)
|
|
||||||
without_legacy_reads { native_get(path, action:) }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to eq(original)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'missing native resources' do
|
|
||||||
it 'returns 404 for a Tag without a description' do
|
|
||||||
without_legacy_reads { native_get(endpoint) }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 404 for a primary name without a description' do
|
|
||||||
tag
|
|
||||||
without_legacy_reads do
|
|
||||||
native_get("/tags/name/#{ tag_name.name }/description", action: 'show_by_title')
|
|
||||||
end
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 404 for a missing Tag' do
|
|
||||||
missing_id = Tag.maximum(:id).to_i + 1
|
|
||||||
without_legacy_reads { native_get("/tags/#{ missing_id }/description") }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'PUT /tags/:id/description' do
|
|
||||||
context 'without an existing description' do
|
|
||||||
before { tag }
|
|
||||||
|
|
||||||
it 'creates both current resources and histories with matching compatibility IDs' do
|
|
||||||
expect { native_write(body: 'created natively') }
|
|
||||||
.to change(TagDescription, :count).by(1)
|
|
||||||
.and change(TagDescriptionVersion, :count).by(1)
|
|
||||||
.and change(WikiPage, :count).by(1)
|
|
||||||
.and change(WikiRevision, :count).by(1)
|
|
||||||
.and change(WikiVersion, :count).by(1)
|
|
||||||
expect(response).to have_http_status(:created)
|
|
||||||
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
|
||||||
page = WikiPage.find(description.id)
|
|
||||||
revision = page.current_revision
|
|
||||||
version = TagDescriptionVersion.find(revision.id)
|
|
||||||
expect(description).to have_attributes(
|
|
||||||
body: 'created natively', version_no: 1,
|
|
||||||
created_by_user_id: editor.id, updated_by_user_id: editor.id)
|
|
||||||
expect(version).to have_attributes(
|
|
||||||
tag_id: tag.id, language_code: 'ja', body: description.body,
|
|
||||||
version_no: 1, event_type: 'create', created_by_user_id: editor.id)
|
|
||||||
expect(page).to have_attributes(tag_name_id: tag_name.id, body: description.body)
|
|
||||||
expect(revision.body).to eq(version.body)
|
|
||||||
expect(page.wiki_versions.last.body).to eq(version.body)
|
|
||||||
expect_current(description)
|
|
||||||
|
|
||||||
get "/wiki/#{ description.id }"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('body' => description.body, 'revision_id' => version.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
|
||||||
it "rolls back native create when #{ model.name } persistence fails" do
|
|
||||||
headers
|
|
||||||
before_state = stored_state
|
|
||||||
aggregate_failures do
|
|
||||||
fail_save(model) { native_write(body: 'rollback create') }
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with an existing description' do
|
|
||||||
let!(:page) do
|
|
||||||
tag
|
|
||||||
Wiki::Commit.create_content!(tag_name:, body: 'before', created_by_user: author)
|
|
||||||
end
|
|
||||||
let!(:description) do
|
|
||||||
TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'writes matching histories when the body changes' do
|
|
||||||
expect(page.reload.version_no).to eq(1)
|
|
||||||
expect(description.version_no).to eq(1)
|
|
||||||
expect { native_write(body: 'after') }
|
|
||||||
.to change(WikiRevision, :count).by(1)
|
|
||||||
.and change(WikiVersion, :count).by(1)
|
|
||||||
.and change(TagDescriptionVersion, :count).by(1)
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(description.reload).to have_attributes(
|
|
||||||
body: 'after', version_no: 2,
|
|
||||||
created_by_user_id: author.id, updated_by_user_id: editor.id)
|
|
||||||
expect(page.reload).to have_attributes(body: 'after', version_no: 2)
|
|
||||||
|
|
||||||
revision = page.current_revision
|
|
||||||
version = TagDescriptionVersion.find(revision.id)
|
|
||||||
expect(version).to have_attributes(
|
|
||||||
tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update',
|
|
||||||
body: 'after', created_by_user_id: editor.id)
|
|
||||||
expect(revision.body).to eq(version.body)
|
|
||||||
expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body)
|
|
||||||
get "/wiki/#{ description.id }"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('body' => 'after', 'revision_id' => version.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
['before', "before\n\n"].each do |body|
|
|
||||||
it 'does not write histories when the normalised body is unchanged' do
|
|
||||||
before_state = stored_state
|
|
||||||
page_state = page.reload.attributes
|
|
||||||
description_state = description.reload.attributes
|
|
||||||
current_revision_id = page.current_revision.id
|
|
||||||
|
|
||||||
native_write(body:)
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
expect(page.reload.attributes).to eq(page_state)
|
|
||||||
expect(description.reload.attributes).to eq(description_state)
|
|
||||||
expect(page.current_revision.id).to eq(current_revision_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
|
||||||
it "rolls back native update when #{ model.name } persistence fails" do
|
|
||||||
headers
|
|
||||||
before_state = stored_state
|
|
||||||
aggregate_failures do
|
|
||||||
fail_save(model) { native_write(body: 'rollback update') }
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'write access and validation' do
|
|
||||||
before { tag }
|
|
||||||
|
|
||||||
it 'requires authentication without changing either system' do
|
|
||||||
before_state = stored_state
|
|
||||||
native_write(body: 'unauthorised', request_headers: { })
|
|
||||||
expect(response).to have_http_status(:unauthorized)
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a guest without changing either system' do
|
|
||||||
guest = create(:user, :guest)
|
|
||||||
before_state = stored_state
|
|
||||||
native_write(body: 'forbidden',
|
|
||||||
request_headers: { 'X-Transfer-Code' => guest.inheritance_code })
|
|
||||||
expect(response).to have_http_status(:forbidden)
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects a banned member without changing either system' do
|
|
||||||
banned = create(:user, :member, :banned)
|
|
||||||
before_state = stored_state
|
|
||||||
native_write(body: 'forbidden',
|
|
||||||
request_headers: { 'X-Transfer-Code' => banned.inheritance_code })
|
|
||||||
expect(response).to have_http_status(:forbidden)
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects blank body using the existing validation error format' do
|
|
||||||
headers
|
|
||||||
before_state = stored_state
|
|
||||||
native_write(body: '')
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
|
||||||
expect(json).to include('type' => 'validation_error',
|
|
||||||
'errors' => include('body' => a_kind_of(Array)))
|
|
||||||
expect(stored_state).to eq(before_state)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 404 when PUT targets a missing Tag' do
|
|
||||||
missing_id = Tag.maximum(:id).to_i + 1
|
|
||||||
put "/tags/#{ missing_id }/description",
|
|
||||||
params: { body: 'missing' },
|
|
||||||
headers: headers
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :request do
|
|
||||||
include TagGroupRecords
|
|
||||||
|
|
||||||
let!(:user) { create_member_user! }
|
|
||||||
let!(:series) { group_tag('ぼざろクリーチャーシリーズ') }
|
|
||||||
let!(:deer) { group_tag('洗澡鹿') }
|
|
||||||
let!(:nijika) { group_tag('伊地知ニジカ') }
|
|
||||||
let!(:wanted) { group_tag('タグ希望') }
|
|
||||||
let!(:character) { group_tag('キャラクター名希望') }
|
|
||||||
let!(:music) { group_tag('楽曲情報希望') }
|
|
||||||
let!(:bot) { group_tag('bot操作') }
|
|
||||||
let!(:unknown) { group_tag('ニジラー情報不詳') }
|
|
||||||
let!(:child_id) { seed_group(name: '伊地知ニジカ亜種', description: '子の説明') }
|
|
||||||
|
|
||||||
before { I18n.locale = :ja }
|
|
||||||
|
|
||||||
def tag_node tag, children = []
|
|
||||||
{ 'tag_id' => tag.id, 'children' => children }
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_member tag
|
|
||||||
{ 'tag_id' => tag.id, 'version_no' => tag.version_no }
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_round_trip placements:, members:, markdown:, description: nil
|
|
||||||
id = seed_group(name: '変換元', placements:, members:, description:)
|
|
||||||
without_group_legacy_reads { get group_title_path('変換元') }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('id' => id)
|
|
||||||
body = json.fetch('body')
|
|
||||||
expect(body).not_to include("\r")
|
|
||||||
expect(body.lines.map(&:chomp)).to all(satisfy { |line| line == line.rstrip })
|
|
||||||
# A trailing LF or a blank line between blocks is semantically irrelevant.
|
|
||||||
expect(body.lines.map(&:chomp).reject(&:empty?)).to eq(
|
|
||||||
markdown.lines.map(&:chomp).reject(&:empty?))
|
|
||||||
|
|
||||||
post '/wiki', params: { title: 'タググループ:再解析', body: },
|
|
||||||
headers: { 'X-Transfer-Code' => user.inheritance_code }
|
|
||||||
expect(response).to have_http_status(:created)
|
|
||||||
parsed = group_rows('tag_groups', name: '再解析').sole
|
|
||||||
expect_group_snapshot(parsed.fetch('id'), number: 1, placements:, members:, description:)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders a list Section under a Tag with two-space indentation' do
|
|
||||||
check_round_trip(
|
|
||||||
members: [series, deer].map { tag_member(_1) },
|
|
||||||
placements: [tag_node(series, [
|
|
||||||
{ 'section_name' => '伊地知ニジカ亜種', 'description' => nil,
|
|
||||||
'children' => [tag_node(deer)] }])],
|
|
||||||
markdown: <<~MARKDOWN)
|
|
||||||
- ぼざろクリーチャーシリーズ
|
|
||||||
- 伊地知ニジカ亜種:
|
|
||||||
- 洗澡鹿
|
|
||||||
MARKDOWN
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders a child Tag Group distinctly from its parent Tags' do
|
|
||||||
check_round_trip(
|
|
||||||
members: [tag_member(series), tag_member(nijika),
|
|
||||||
{ 'tag_group_id' => child_id, 'version_no' => 1 }],
|
|
||||||
placements: [tag_node(series, [tag_node(nijika, [
|
|
||||||
{ 'tag_group_id' => child_id, 'children' => [] }])])],
|
|
||||||
markdown: <<~MARKDOWN)
|
|
||||||
- ぼざろクリーチャーシリーズ
|
|
||||||
- 伊地知ニジカ
|
|
||||||
- タググループ:伊地知ニジカ亜種
|
|
||||||
MARKDOWN
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders top-level heading Sections, descriptions and ordered nested Tags' do
|
|
||||||
check_round_trip(
|
|
||||||
description: '全体の説明',
|
|
||||||
members: [wanted, character, music, bot, unknown].map { tag_member(_1) },
|
|
||||||
placements: [
|
|
||||||
{ 'section_name' => '要求', 'description' => '要求の説明',
|
|
||||||
'children' => [tag_node(wanted, [tag_node(character), tag_node(music)])] },
|
|
||||||
{ 'section_name' => '情報', 'description' => nil,
|
|
||||||
'children' => [tag_node(bot), tag_node(unknown)] }],
|
|
||||||
markdown: <<~MARKDOWN)
|
|
||||||
全体の説明
|
|
||||||
|
|
||||||
# 要求
|
|
||||||
要求の説明
|
|
||||||
|
|
||||||
- タグ希望
|
|
||||||
- キャラクター名希望
|
|
||||||
- 楽曲情報希望
|
|
||||||
|
|
||||||
# 情報
|
|
||||||
- bot操作
|
|
||||||
- ニジラー情報不詳
|
|
||||||
MARKDOWN
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders nested heading Sections and all repeated Tag placements' do
|
|
||||||
check_round_trip(
|
|
||||||
members: [tag_member(deer)],
|
|
||||||
placements: [
|
|
||||||
{ 'section_name' => '親', 'description' => '親の説明', 'children' => [
|
|
||||||
tag_node(deer),
|
|
||||||
{ 'section_name' => '子', 'description' => '子の説明',
|
|
||||||
'children' => [tag_node(deer)] }] }],
|
|
||||||
markdown: <<~MARKDOWN)
|
|
||||||
# 親
|
|
||||||
親の説明
|
|
||||||
|
|
||||||
- 洗澡鹿
|
|
||||||
|
|
||||||
## 子
|
|
||||||
子の説明
|
|
||||||
|
|
||||||
- 洗澡鹿
|
|
||||||
MARKDOWN
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects mixed top-level heading Sections and Tags without losing structure' do
|
|
||||||
id = seed_group(
|
|
||||||
name: '混在構造', members: [tag_member(series)],
|
|
||||||
placements: [
|
|
||||||
{ 'section_name' => '区分', 'description' => nil, 'children' => [] },
|
|
||||||
tag_node(series)])
|
|
||||||
|
|
||||||
get group_title_path('混在構造')
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
|
||||||
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects membership without a placement instead of omitting it' do
|
|
||||||
id = seed_group(name: '未配置構造', members: [tag_member(series)])
|
|
||||||
|
|
||||||
get group_title_path('未配置構造')
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
|
||||||
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'Tag group Wiki facade reads', type: :request do
|
|
||||||
include TagGroupRecords
|
|
||||||
|
|
||||||
let!(:user) { create_member_user! }
|
|
||||||
let!(:old_tag) { group_tag('旧タグ') }
|
|
||||||
let!(:new_tag) { group_tag('新タグ') }
|
|
||||||
let!(:page) { legacy_group('読取り契約', '- 旧タグ') }
|
|
||||||
let!(:first_revision) { page.current_revision }
|
|
||||||
let!(:last_revision) do
|
|
||||||
Wiki::Commit.legacy_content!(page:, body: '- 新タグ', created_user: user)
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
I18n.locale = :ja
|
|
||||||
migrate_groups
|
|
||||||
page.update_columns(body: 'legacy stale current')
|
|
||||||
page.wiki_revisions.each do |revision|
|
|
||||||
revision.wiki_lines.update_all(body: 'legacy stale revision')
|
|
||||||
end
|
|
||||||
page.wiki_versions.update_all(body: 'legacy stale version')
|
|
||||||
end
|
|
||||||
|
|
||||||
[:id, :title].each do |lookup|
|
|
||||||
context "with #{ lookup } lookup" do
|
|
||||||
let(:path) { lookup == :id ? "/wiki/#{ page.id }" : group_title_path('読取り契約') }
|
|
||||||
|
|
||||||
it 'reads current structure without consulting migrated Wiki tables' do
|
|
||||||
without_group_legacy_reads { get path }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include(
|
|
||||||
'id' => page.id, 'title' => 'タググループ:読取り契約',
|
|
||||||
'revision_id' => last_revision.id, 'pred' => first_revision.id, 'succ' => nil)
|
|
||||||
expect(json.fetch('body').strip).to eq('- 新タグ')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'reads the old snapshot rather than current membership or placement' do
|
|
||||||
without_group_legacy_reads { get path, params: { version: first_revision.id } }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.fetch('body').strip).to eq('- 旧タグ')
|
|
||||||
expect(json).to include(
|
|
||||||
'revision_id' => first_revision.id, 'pred' => nil, 'succ' => last_revision.id,
|
|
||||||
'updated_at' => first_revision.created_at.iso8601(3))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'checks existence without consulting migrated Wiki tables' do
|
|
||||||
without_group_legacy_reads { get "#{ path }/exists" }
|
|
||||||
expect(response).to have_http_status(:no_content)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not fall back to legacy history when a group version is missing' do
|
|
||||||
group_connection.execute(
|
|
||||||
"DELETE FROM tag_group_versions WHERE id = #{ first_revision.id }")
|
|
||||||
without_group_legacy_reads { get path, params: { version: first_revision.id } }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
['/wiki', '/wiki/search'].each do |path|
|
|
||||||
it "lists migrated groups once with current titles at #{ path }" do
|
|
||||||
get path, params: { title: 'タググループ:読取り' }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
rows = json.select { _1.fetch('title') == 'タググループ:読取り契約' }
|
|
||||||
expect(rows.size).to eq(1)
|
|
||||||
expect(rows.sole).to include('id' => page.id)
|
|
||||||
expect(response.body).not_to include('legacy stale')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'combines Tag Groups, Help and Tag Descriptions in the unfiltered list' do
|
|
||||||
help = Wiki::Commit.create_legacy_content!(
|
|
||||||
tag_name: create(:tag_name, name: 'Help:混在'), body: 'help', created_by_user: user)
|
|
||||||
TagDescriptionOperation.create!(tag: new_tag, body: 'tag description', created_by_user: user)
|
|
||||||
get '/wiki'
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.map { _1.fetch('title') }).to include(
|
|
||||||
'タググループ:読取り契約', help.title, '新タグ')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'keeps Help readable when its ID collides with a new group' do
|
|
||||||
help = Wiki::Commit.create_legacy_content!(
|
|
||||||
tag_name: create(:tag_name, name: 'Help:ID重複'), body: 'help body',
|
|
||||||
created_by_user: user)
|
|
||||||
seed_group(name: '別 ID重複', id: help.id, description: 'group body')
|
|
||||||
|
|
||||||
get '/wiki'
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.map { _1.fetch('title') }).to include(help.title, 'タググループ:別 ID重複')
|
|
||||||
|
|
||||||
get "/wiki/title/#{ CGI.escapeURIComponent(help.title) }"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.fetch('body')).to eq('help body')
|
|
||||||
|
|
||||||
get '/wiki/changes'
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.map { _1.dig('wiki_page', 'title') }).to include(
|
|
||||||
help.title, 'タググループ:別 ID重複')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'finds groups by a partial prefix of their displayed title' do
|
|
||||||
get '/wiki', params: { title: 'タググル' }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.map { _1.fetch('title') }).to include('タググループ:読取り契約')
|
|
||||||
expect(json.find { _1.fetch('title') == 'タググループ:読取り契約' })
|
|
||||||
.to include('updated_at' => be_present)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not restore a stale namespaced Wiki page after a group rename' do
|
|
||||||
old_page = legacy_group('旧名称', '- 旧本文')
|
|
||||||
seed_group(name: '新名称', id: old_page.id, description: '正本')
|
|
||||||
|
|
||||||
get group_title_path('旧名称')
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
get "#{ group_title_path('旧名称') }/exists"
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
get group_title_path('新名称')
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.fetch('body').strip).to eq('正本')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'orders mixed resource history by timestamp rather than independent IDs' do
|
|
||||||
help = Wiki::Commit.create_legacy_content!(
|
|
||||||
tag_name: create(:tag_name, name: 'Help:時系列'), body: 'help',
|
|
||||||
created_by_user: user)
|
|
||||||
TagDescriptionOperation.create!(tag: new_tag, body: 'description', created_by_user: user)
|
|
||||||
TagGroupVersion.update_all(created_at: 3.days.ago)
|
|
||||||
WikiRevision.where(wiki_page_id: help.id).update_all(created_at: 2.days.ago)
|
|
||||||
TagDescriptionVersion.update_all(created_at: 1.day.ago)
|
|
||||||
|
|
||||||
get '/wiki/changes'
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
timestamps = json.map { Time.iso8601(_1.fetch('timestamp')) }
|
|
||||||
expect(timestamps).to eq(timestamps.sort.reverse)
|
|
||||||
titles = json.map { _1.dig('wiki_page', 'title') }
|
|
||||||
expect(titles.index('新タグ')).to be < titles.index(help.title)
|
|
||||||
expect(titles.index(help.title)).to be < titles.index('タググループ:読取り契約')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders nullable group editors without raising' do
|
|
||||||
group_connection.execute(
|
|
||||||
"UPDATE tag_groups SET created_by_user_id = NULL, updated_by_user_id = NULL " +
|
|
||||||
"WHERE id = #{ page.id }")
|
|
||||||
group_connection.execute(
|
|
||||||
"UPDATE tag_group_versions SET created_by_user_id = NULL " +
|
|
||||||
"WHERE tag_group_id = #{ page.id }")
|
|
||||||
|
|
||||||
get "/tag_groups/#{ page.id }"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('created_by_user' => nil, 'updated_by_user' => nil)
|
|
||||||
get "/wiki/title/#{ CGI.escapeURIComponent('タググループ:読取り契約') }"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'reads group version IDs, editor and timestamps in scoped changes' do
|
|
||||||
without_group_legacy_reads { get '/wiki/changes', params: { id: page.id } }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.map { _1.fetch('revision_id') }).to eq([last_revision.id, first_revision.id])
|
|
||||||
expect(json.first).to include(
|
|
||||||
'pred' => first_revision.id, 'succ' => nil, 'kind' => 'content',
|
|
||||||
'timestamp' => last_revision.created_at.iso8601(3),
|
|
||||||
'user' => { 'id' => user.id, 'name' => user.name },
|
|
||||||
'wiki_page' => include('id' => page.id, 'title' => 'タググループ:読取り契約'))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'includes Help and group history but excludes legacy-only migrated revisions' do
|
|
||||||
extra = Wiki::Commit.legacy_content!(page:, body: 'legacy only', created_user: user)
|
|
||||||
help = Wiki::Commit.create_legacy_content!(
|
|
||||||
tag_name: create(:tag_name, name: 'Help:変更一覧'), body: 'help', created_by_user: user)
|
|
||||||
get '/wiki/changes'
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
rows = json.select { _1.fetch('wiki_page').fetch('title') == 'タググループ:読取り契約' }
|
|
||||||
expect(rows.map { _1.fetch('revision_id') }).to eq([last_revision.id, first_revision.id])
|
|
||||||
expect(rows.map { _1.fetch('revision_id') }).not_to include(extra.id)
|
|
||||||
expect(json).to include(include(
|
|
||||||
'revision_id' => help.current_revision.id,
|
|
||||||
'wiki_page' => include('title' => help.title)))
|
|
||||||
end
|
|
||||||
|
|
||||||
[false, true].each do |explicit|
|
|
||||||
it "diffs structured snapshots with #{ explicit ? 'explicit' : 'current' } target" do
|
|
||||||
params = { from: first_revision.id }
|
|
||||||
params[:to] = last_revision.id if explicit
|
|
||||||
without_group_legacy_reads { get "/wiki/#{ page.id }/diff", params: }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include(
|
|
||||||
'wiki_page_id' => page.id, 'older_revision_id' => first_revision.id,
|
|
||||||
'newer_revision_id' => last_revision.id)
|
|
||||||
differences = json.fetch('diff').map { [_1.fetch('type'), _1.fetch('content').strip] }
|
|
||||||
expect(differences).to include(['removed', '- 旧タグ'], ['added', '- 新タグ'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not use a version belonging to another group' do
|
|
||||||
other_id = seed_group(name: '別グループ', description: '別本文')
|
|
||||||
other_version = group_version(other_id, 1)
|
|
||||||
get group_title_path('読取り契約'), params: { version: other_version.fetch('id') }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
get "/wiki/#{ page.id }/diff",
|
|
||||||
params: { from: other_version.fetch('id'), to: last_revision.id }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resolves a group without any compatibility Wiki page' do
|
|
||||||
id = seed_group(name: '独立グループ', description: '独立した説明')
|
|
||||||
without_group_legacy_reads { get group_title_path('独立グループ') }
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json).to include('id' => id, 'title' => 'タググループ:独立グループ')
|
|
||||||
expect(json.fetch('body').strip).to eq('独立した説明')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resolves colliding group, Help and description IDs by title' do
|
|
||||||
help = Wiki::Commit.create_legacy_content!(
|
|
||||||
tag_name: create(:tag_name, name: 'Help:ID衝突'), body: 'help', created_by_user: user)
|
|
||||||
seed_group(name: 'ID衝突', id: help.id, description: 'group')
|
|
||||||
TagDescription.create!(
|
|
||||||
id: help.id, tag: new_tag, language_code: 'ja', body: 'description', version_no: 1)
|
|
||||||
TagDescriptionVersion.create!(
|
|
||||||
tag_id: new_tag.id, language_code: 'ja', version_no: 1,
|
|
||||||
event_type: 'create', body: 'description', created_at: Time.current)
|
|
||||||
{ 'タググループ:ID衝突' => 'group', 'Help:ID衝突' => 'help', '新タグ' => 'description' }
|
|
||||||
.each do |title, body|
|
|
||||||
get "/wiki/title/#{ CGI.escapeURIComponent(title) }"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(json.fetch('body').strip).to eq(body)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
変更されたファイルが多すぎるため、一部のファイルは表示されません さらに表示
新しいイシューから参照
ユーザーをブロックする