コミットを比較

..
14 コミット
作成者 SHA1 メッセージ 日付
みてるぞ a0f1d0c4c3 #419 2026-09-24 12:48:29 +09:00
みてるぞ b04f3e698d #419 2026-09-24 08:46:22 +09:00
みてるぞ 5d2e85a690 #419 2026-09-24 08:14:17 +09:00
みてるぞ 3b2acb95be #419 2026-09-24 07:16:00 +09:00
みてるぞ 1de00191ea #419 2026-09-24 07:01:36 +09:00
みてるぞ d1bab299ab #419 フロントのテスト追加 2026-09-23 18:22:24 +09:00
みてるぞ 089727c153 #419 2026-09-23 14:40:59 +09:00
みてるぞ 894f9f9571 #419 2026-09-23 10:59:03 +09:00
みてるぞ 2490ed91c4 #419 2026-09-23 09:41:41 +09:00
みてるぞ 2dbd5260ea #419 2026-09-23 02:46:56 +09:00
みてるぞ 2e25c6e0ba #419 2026-09-22 20:33:46 +09:00
みてるぞ a0c2788e00 #419 2026-09-22 20:16:30 +09:00
みてるぞ 31fc32b377 #419 2026-09-22 19:22:16 +09:00
みてるぞ 068355720d #419 2026-09-22 15:13:14 +09:00
86個のファイルの変更、1017行の追加、2397行の削除
-2
ファイルの表示
@@ -92,6 +92,4 @@ class ApplicationController < ActionController::API
value
end
def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese
end
+3 -6
ファイルの表示
@@ -1,7 +1,5 @@
class DeerjikistsController < ApplicationController
def show
locale = resolve_locale!
platform = params[:platform].to_s.strip
code = params[:code].to_s.strip
return render_bad_request('platform は必須です.') if platform.blank?
@@ -9,10 +7,10 @@ class DeerjikistsController < ApplicationController
deerjikist = Deerjikist
.joins(:tag)
.includes(tag: :tag_names)
.includes(tag: :tag_name)
.find_by(platform:, code:)
if deerjikist
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
render json: DeerjikistRepr.base(deerjikist)
else
head :not_found
end
@@ -22,7 +20,6 @@ class DeerjikistsController < ApplicationController
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
platform = params[:platform].to_s.strip
code = params[:code].to_s.strip
tag_id = params[:tag_id].to_i
@@ -35,7 +32,7 @@ class DeerjikistsController < ApplicationController
d.save!
end
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
render json: DeerjikistRepr.base(deerjikist)
end
def destroy
+2 -2
ファイルの表示
@@ -2,7 +2,7 @@ class GekanatorPostsController < ApplicationController
def index
posts =
Post
.preload(:post_similarities, tags: :tag_names)
.preload(:post_similarities, tags: :tag_name)
.with_attached_thumbnail
.order(Arel.sql(
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
@@ -56,7 +56,7 @@ class GekanatorPostsController < ApplicationController
def tag_json tag
{
id: tag.id,
name: tag.name('ja'),
name: tag.name,
category: tag.category
}
end
+2 -2
ファイルの表示
@@ -126,9 +126,9 @@ class GekanatorQuestionsController < ApplicationController
end
Tag
.joins(:tag_names)
.joins(:tag_name)
.where(category: categories.uniq)
.where(tag_names: { language_code: 'ja', primary_flg: true, name: names.uniq })
.where(tag_names: { name: names.uniq })
.where.not(deprecated_at: nil)
.pluck('tags.category', 'tag_names.name')
.each_with_object({ }) do |(category, name), h|
+20 -41
ファイルの表示
@@ -7,8 +7,6 @@ class MaterialsController < ApplicationController
rescue_from MaterialZipExporter::MissingFileError, with: :render_zip_missing_file
def index
locale = resolve_locale!
page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i
@@ -22,7 +20,7 @@ class MaterialsController < ApplicationController
q = Material.includes(:material_export_items,
thumbnail_attachment: :blob,
file_attachment: :blob,
tag: :tag_names)
tag: :tag_name)
q = q.where(tag_id: nil) if filters[:tag_state] == 'untagged'
q = q.where.not(tag_id: nil) if filters[:tag_state] == 'tagged'
q = apply_material_tag_filter(q, filters, tag_graph)
@@ -44,10 +42,7 @@ class MaterialsController < ApplicationController
.offset(offset)
.to_a
response = { materials: MaterialRepr.list_many(
materials,
host: request.base_url,
language_code: locale.language_code),
response = { materials: MaterialRepr.list_many(materials, host: request.base_url),
count: }
if filters[:tag_id]
response[:tag_scope] = material_index_tag_scope(filters, tag_graph)
@@ -60,8 +55,6 @@ class MaterialsController < ApplicationController
end
def show
locale = resolve_locale!
material =
Material
.includes(:tag, :material_export_items)
@@ -70,17 +63,15 @@ class MaterialsController < ApplicationController
.find_by(id: params[:id])
return head :not_found unless material
wiki_page_body = material.tag&.tag_name('ja')&.wiki_page&.current_revision&.body
wiki_page_body = material.tag&.tag_name&.wiki_page&.current_revision&.body
render json: MaterialRepr.base(material, host: request.base_url,
language_code: locale.language_code).merge(wiki_page_body:)
render json: MaterialRepr.base(material, host: request.base_url).merge(wiki_page_body:)
end
def create
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
tag_name_raw = params[:tag].to_s.strip
file = params[:file]
file_sha256 = MaterialFileSha256.from_upload(file)
@@ -98,14 +89,13 @@ class MaterialsController < ApplicationController
begin
Material.transaction do
tag = resolve_material_tag!(locale, tag_name_raw)
tag = resolve_material_tag!(tag_name_raw)
material = Material.new(tag:, url:,
created_by_user: current_user,
updated_by_user: current_user)
material.file.attach(uploaded_blob) if uploaded_blob
material.save!
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
language_code: locale.language_code)
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
upsert_export_paths!(material)
MaterialVersionRecorder.record!(material:, event_type: :create,
created_by_user: current_user)
@@ -117,8 +107,7 @@ class MaterialsController < ApplicationController
if material
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
render json: MaterialRepr.base(material, host: request.base_url,
language_code: locale.language_code), status: :created
render json: MaterialRepr.base(material, host: request.base_url), status: :created
else
render_validation_error material
end
@@ -143,14 +132,12 @@ class MaterialsController < ApplicationController
block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
return render_material_import_block(block) if block
locale = resolve_locale!
uploaded_blob = build_uploaded_material_blob!(file, file_sha256)
begin
Material.transaction do
MaterialVersionRecorder.ensure_snapshot!(material, created_by_user: current_user)
tag = resolve_material_tag!(locale, tag_name_raw)
tag = resolve_material_tag!(tag_name_raw)
material.assign_attributes(tag:, url:, updated_by_user: current_user)
if uploaded_blob
material.file.attach(uploaded_blob)
@@ -158,8 +145,7 @@ class MaterialsController < ApplicationController
material.file.detach
end
material.save!
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
language_code: locale.language_code)
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
upsert_export_paths!(material)
MaterialVersionRecorder.record!(material:, event_type: :update,
created_by_user: current_user)
@@ -171,8 +157,7 @@ class MaterialsController < ApplicationController
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
render json: MaterialRepr.base(material, host: request.base_url,
language_code: locale.language_code)
render json: MaterialRepr.base(material, host: request.base_url)
end
def destroy
@@ -251,8 +236,10 @@ class MaterialsController < ApplicationController
nil
end
def resolve_material_tag! locale, tag_name_raw
Tag.find_or_create_by_tag_name!(locale, tag_name_raw, category: :material)
def resolve_material_tag! tag_name_raw
tag_name = TagName.find_or_create_by!(name: tag_name_raw)
tag = tag_name.tag
tag || Tag.create!(tag_name:, category: :material)
end
def material_index_needs_tag_name? filters
@@ -266,12 +253,7 @@ class MaterialsController < ApplicationController
end
def material_index_join_tag_name q
q.left_joins(:tag).joins(<<~SQL.squish)
LEFT JOIN tag_names
ON tag_names.tag_id = tags.id
AND tag_names.language_code = 'ja'
AND tag_names.primary_flg = TRUE
SQL
q.left_joins(tag: :tag_name)
end
def material_index_join_file_blob q
@@ -287,11 +269,9 @@ class MaterialsController < ApplicationController
def apply_material_query q, term
like = "%#{ ActiveRecord::Base.sanitize_sql_like(term) }%"
q.where(<<~SQL.squish, q: like)
tag_names.name LIKE :q
OR materials.url LIKE :q
OR material_file_blobs.filename LIKE :q
SQL
q.where('tag_names.name LIKE :q OR materials.url LIKE :q OR ' \
'material_file_blobs.filename LIKE :q',
q: like)
end
def apply_material_media_kind q, media_kind
@@ -343,9 +323,8 @@ class MaterialsController < ApplicationController
end
tags_by_id =
Tag
.joins(:tag_names)
.where(id: scope_tag_ids,
tag_names: { language_code: 'ja', primary_flg: true })
.joins(:tag_name)
.where(id: scope_tag_ids)
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
.each_with_object({ }) do |(id, name, category, deprecated_at), hash|
hash[id] = { id:, name:, category:, deprecated: deprecated_at.present? }
+6 -19
ファイルの表示
@@ -1,7 +1,5 @@
class NicoTagsController < ApplicationController
def index
locale = resolve_locale!
name = params[:name].presence
linked_tag = params[:linked_tag].presence
link_status = params[:link_status].presence
@@ -26,7 +24,7 @@ class NicoTagsController < ApplicationController
ExternalTag
.joins("LEFT JOIN (#{ post_tag_max_sql }) post_tag_max " \
'ON post_tag_max.external_tag_id = external_tags.id')
.includes(linked_tags: { tag_names: :wiki_page })
.includes(linked_tags: { tag_name: :wiki_page })
if name
q = q.where(('external_tags.name LIKE ? ' +
"OR CONCAT(external_tags.platform, ':', external_tags.name) LIKE ?"),
@@ -36,8 +34,7 @@ class NicoTagsController < ApplicationController
if linked_tag
linked_tag_ids =
Tag
.joins(:tag_names)
.where(tag_names: { language_code: 'ja', primary_flg: true })
.joins(:tag_name)
.where('tag_names.name LIKE ?', "%#{ linked_tag }%")
.pluck(:id)
linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id)
@@ -70,9 +67,7 @@ class NicoTagsController < ApplicationController
render json: { tags: tags.map { |tag|
external_tag_json(tag).merge(
recent_post_tag_created_at: tag.recent_post_tag_created_at,
linked_tags: tag.linked_tags.map { |lt|
TagRepr.base(lt, language_code: locale.language_code)
})
linked_tags: tag.linked_tags.map { |lt| TagRepr.base(lt) })
}, count: }
end
@@ -86,8 +81,6 @@ class NicoTagsController < ApplicationController
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
id = params[:id].to_i
tag = ExternalTag.find(id)
@@ -96,14 +89,10 @@ class NicoTagsController < ApplicationController
linked_tags = nil
ApplicationRecord.transaction do
linked_tags = Tag.normalise_tags!(locale, linked_tag_names,
with_tagme: false,
linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false,
with_no_deerjikist: false)
TagVersioning.record_tag_snapshots!(
linked_tags,
created_by_user: current_user,
language_code: locale.language_code)
TagVersioning.record_tag_snapshots!(linked_tags, created_by_user: current_user)
tag.linked_tags = linked_tags
tag.save!
@@ -113,9 +102,7 @@ class NicoTagsController < ApplicationController
created_by_user: current_user)
end
render json: tag.linked_tags.map { |t|
TagRepr.base(t, language_code: locale.language_code)
}, status: :ok
render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok
rescue Tag::NicoTagNormalisationError
render_validation_error fields: { tags: ['ニコニコ・タグ同士は連携できません.'] }
rescue ActiveRecord::RecordInvalid => e
+45 -69
ファイルの表示
@@ -7,8 +7,6 @@ class PostsController < ApplicationController
end
def index
locale = resolve_locale!
url = params[:url].presence
title = params[:title].presence
original_created_from = params[:original_created_from].presence
@@ -48,12 +46,12 @@ class PostsController < ApplicationController
'COALESCE(pt_max.max_updated_at, posts.updated_at))'
q =
filtered_posts(locale)
filtered_posts
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
.preload(:external_tags, :uploaded_user, :parents, :children,
post_tags: [:sections, { tag: [:deerjikists, :materials,
{ tag_names: :wiki_page }] }])
{ tag_name: :wiki_page }] }])
.with_attached_thumbnail
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
@@ -92,8 +90,7 @@ class PostsController < ApplicationController
q = q.except(:select, :order)
render json: { posts: posts.map { |post|
PostRepr.base(post, language_code: locale.language_code)
.merge(updated_at: post.updated_at_all).tap do |json|
PostRepr.base(post).merge(updated_at: post.updated_at_all).tap do |json|
json['thumbnail'] =
if post.thumbnail.attached?
rails_storage_proxy_url(post.thumbnail, only_path: false)
@@ -105,19 +102,17 @@ class PostsController < ApplicationController
end
def random
locale = resolve_locale!
post =
filtered_posts(locale)
filtered_posts
.preload(:uploaded_user, :parents, :children,
post_tags: [:sections, { tag: [:deerjikists, :materials,
{ tag_names: :wiki_page }] }])
{ tag_name: :wiki_page }] }])
.with_attached_thumbnail
.order('RAND()')
.first
return head :not_found unless post
render json: PostRepr.base(post, current_user, language_code: locale.language_code)
render json: PostRepr.base(post, current_user)
end
def metadata
@@ -192,13 +187,11 @@ class PostsController < ApplicationController
end
def show
locale = resolve_locale!
post =
Post
.includes(:uploaded_user, :parents, :children,
post_tags: [:sections, { tag: [:deerjikists, :materials,
{ tag_names: :wiki_page }] }])
{ tag_name: :wiki_page }] }])
.with_attached_thumbnail
.find_by(id: params[:id])
return head :not_found unless post
@@ -212,21 +205,18 @@ class PostsController < ApplicationController
parent_posts:,
child_posts:,
sibling_posts:,
related:,
language_code: locale.language_code)
.merge(tags: build_tag_tree_for(post, locale.language_code))
related:)
.merge(tags: build_tag_tree_for(post))
end
def create
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
preflight = PostCreatePreflight.new(
attributes: post_create_attributes,
thumbnail: params[:thumbnail],
host: request.base_url).run(locale)
host: request.base_url).run
return render json: dry_run_json(preflight) if bool?(:dry)
if preflight[:existing_post_id].present?
post = Post.new(url: preflight[:url])
@@ -252,10 +242,10 @@ class PostsController < ApplicationController
:post_tag_specs,
:tag_sections,
:normalised_parent_post_ids).symbolize_keys).merge(
thumbnail: params[:thumbnail])).create!(locale)
thumbnail: params[:thumbnail])).create!
post.reload
render json: PostRepr.base(post, language_code: locale.language_code), status: :created
render json: PostRepr.base(post), status: :created
rescue PostCreatePreflight::ValidationFailed => e
render_validation_error fields: e.fields, base: e.base_errors
rescue Tag::NicoTagNormalisationError
@@ -281,18 +271,13 @@ class PostsController < ApplicationController
return head :forbidden unless current_user.gte_member?
return head :unsupported_media_type unless request.content_mime_type == Mime[:multipart_form]
return head :payload_too_large if request.content_length.to_i > MAX_BULK_REQUEST_BYTES
locale = resolve_locale!
posts = parse_bulk_posts_manifest
thumbnails = parse_bulk_thumbnails(posts.length)
result = PostBulkCreator.new(
actor: current_user,
posts:,
thumbnails:,
host: request.base_url).run(locale)
host: request.base_url).run
render json: result
rescue JSON::ParserError
render_bad_request 'posts manifest の JSON が不正です.'
@@ -325,7 +310,6 @@ class PostsController < ApplicationController
base_version_no = parse_base_version_no
return render_bad_request('base_version_no は必須です.') if !(force) && !(base_version_no)
locale = Locale.find_by(code: params[:locale].presence) || Locale.nipponese
title = params[:title].presence
tag_names = params[:tags].to_s.split
original_created_from = params[:original_created_from]
@@ -345,10 +329,9 @@ class PostsController < ApplicationController
base_version = post.post_versions.find_by!(version_no: base_version_no)
base_snapshot = post_snapshot_from_version(base_version)
current_snapshot = post_snapshot_from_record(post, locale)
current_snapshot = post_snapshot_from_record(post)
end
incoming_snapshot = post_incoming_snapshot(locale:,
title:,
incoming_snapshot = post_incoming_snapshot(title:,
original_created_from:,
original_created_before:,
tag_names:,
@@ -377,14 +360,14 @@ class PostsController < ApplicationController
end
end
apply_post_snapshot!(locale, post, snapshot_to_apply)
apply_post_snapshot!(post, snapshot_to_apply)
end
return render json: conflict_json, status: :conflict if conflict_json
post.reload
json = PostRepr.base(post, current_user, language_code: locale.language_code)
json['tags'] = build_tag_tree_for(post, locale.language_code)
json = PostRepr.base(post, current_user)
json['tags'] = build_tag_tree_for(post)
render json:, status: :ok
rescue Tag::NicoTagNormalisationError
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
@@ -402,19 +385,19 @@ class PostsController < ApplicationController
private
def filtered_posts locale
def filtered_posts
tag_names = params[:tags].to_s.split
match_type = params[:match]
if tag_names.present?
filter_posts_by_tags(locale, tag_names, match_type)
filter_posts_by_tags(tag_names, match_type)
else
Post.all
end
end
def filter_posts_by_tags locale, tag_names, match_type
def filter_posts_by_tags tag_names, match_type
literals = tag_names.map do |raw_name|
{ name: TagName.canonicalise(locale, raw_name.sub(/\Anot:/i, '')).first,
{ name: TagName.canonicalise(raw_name.sub(/\Anot:/i, '')).first,
negative: raw_name.downcase.start_with?('not:') }
end
@@ -422,11 +405,11 @@ class PostsController < ApplicationController
if match_type == 'any'
literals.reduce(Post.none) do |posts, literal|
posts.or(tag_literal_relation(locale, literal[:name], negative: literal[:negative]))
posts.or(tag_literal_relation(literal[:name], negative: literal[:negative]))
end
else
literals.reduce(Post.all) do |posts, literal|
ids = tagged_post_ids_for(locale, literal[:name])
ids = tagged_post_ids_for(literal[:name])
if literal[:negative]
posts.where.not(id: ids)
else
@@ -436,8 +419,8 @@ class PostsController < ApplicationController
end
end
def tag_literal_relation locale, name, negative:
ids = tagged_post_ids_for(locale, name)
def tag_literal_relation name, negative:
ids = tagged_post_ids_for(name)
if negative
Post.where.not(id: ids)
else
@@ -445,11 +428,11 @@ class PostsController < ApplicationController
end
end
def tagged_post_ids_for locale, name
def tagged_post_ids_for(name)
posts_by_internal_tags =
Post
.joins(tags: :tag_names)
.where(tag_names: { language_code: locale.language_code, name: })
.joins(tags: :tag_name)
.where(tag_names: { name: })
.select(:id)
posts_by_external_tags =
@@ -491,7 +474,7 @@ class PostsController < ApplicationController
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? }
tags = post_tags.map(&:tag)
tag_ids = tags.map(&:id)
@@ -521,7 +504,7 @@ class PostsController < ApplicationController
sections = sections_by_tag_id.fetch(tag_id, [])
if path.include?(tag_id)
return TagRepr.inline(tag, language_code:).merge(children: [], sections:)
return TagRepr.inline(tag).merge(children: [], sections:)
end
if memo.key?(tag_id)
@@ -533,7 +516,7 @@ class PostsController < ApplicationController
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
memo[tag_id] = TagRepr.inline(tag, language_code:).merge(children:, sections:)
memo[tag_id] = TagRepr.inline(tag).merge(children:, sections:)
end
internal_tags = root_ids.filter_map { |id| build_node.call(id, []) }
@@ -689,45 +672,39 @@ class PostsController < ApplicationController
version.tags_json
.select { _1.key?('tag_id') }
.map { Post.tag_snapshot_literal(_1) }
.compact
.sort
end
def post_snapshot_from_record post, locale
def post_snapshot_from_record post
{ title: post.title,
video_ms: post.video_ms,
original_created_from: snapshot_time(post.original_created_from),
original_created_before: snapshot_time(post.original_created_before),
tag_names: editable_tag_names_from_post(post, locale.language_code),
tag_names: editable_tag_names_from_post(post),
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
end
def editable_tag_names_from_post post, language_code
def editable_tag_names_from_post post
post
.post_tags
.joins(tag: :tag_names)
.joins(tag: :tag_name)
.merge(Tag.where(deprecated_at: nil))
.where(tag_names: { language_code:, primary_flg: true })
.includes(:sections, tag: :tag_names)
.includes(:sections, tag: :tag_name)
.order('tag_names.name')
.map do |post_tag|
name = post_tag.tag.name(language_code)
name = post_tag.tag.tag_name.name
sections = post_tag.sections.sort_by(&:begin_ms)
next name if sections.empty?
"#{ name }#{ sections.map { Post.section_literal(_1) }.join }"
end.compact.sort
end
end
def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:,
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
validate_original_created_values!(original_created_from, original_created_before)
Tag.normalise_tags!(locale, tag_names,
with_tagme: false,
with_no_deerjikist: false,
deny_deprecated: true,
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
with_sections: true) => { tags:, sections: }
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_before: snapshot_time(original_created_before),
tag_names: tags.uniq(&:id).map { |tag|
"#{ tag.name(locale.language_code) }#{ sections[tag.id].to_a.map { section_literal(_1) }.join }"
"#{ tag.name }#{ sections[tag.id].to_a.map { section_literal(_1) }.join }"
}.sort,
parent_post_ids: parent_post_ids.sort }
end
@@ -863,7 +840,7 @@ class PostsController < ApplicationController
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
end
def apply_post_snapshot! locale, post, snapshot
def apply_post_snapshot! post, snapshot
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
post.update!(title: snapshot[:title],
@@ -871,12 +848,11 @@ class PostsController < ApplicationController
original_created_from: snapshot[:original_created_from],
original_created_before: snapshot[:original_created_before])
Tag.normalise_tags!(locale, snapshot[:tag_names],
Tag.normalise_tags!(snapshot[:tag_names],
with_tagme: false,
deny_deprecated: true,
with_sections: true) => { tags:, sections: }
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user,
language_code: locale.language_code)
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user)
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
+8 -21
ファイルの表示
@@ -1,6 +1,5 @@
class TagVersionsController < ApplicationController
def index
locale = resolve_locale!
tag_id = params[:id].presence
page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i
@@ -19,8 +18,7 @@ class TagVersionsController < ApplicationController
SQL
.select('tag_versions.*', 'prev.name AS prev_name', 'prev.category AS prev_category',
'prev.deprecated_at AS prev_deprecated_at',
'prev.aliases AS prev_aliases', 'prev.names AS prev_names',
'prev.parent_tag_ids AS prev_parent_tag_ids')
'prev.aliases AS prev_aliases', 'prev.parent_tag_ids AS prev_parent_tag_ids')
q = q.where('tag_versions.tag_id = ?', tag_id) if tag_id
count = q.except(:select, :order, :limit, :offset).count
@@ -29,20 +27,18 @@ class TagVersionsController < ApplicationController
.limit(limit)
.offset(offset)
render json: { versions: serialise_versions(versions, locale.language_code), count: }
render json: { versions: serialise_versions(versions), count: }
end
private
def serialise_versions rows, language_code
def serialise_versions rows
user_ids = rows.map(&:created_by_user_id).compact.uniq
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
rows.map do |row|
current_names = names_for_locale(row.names, language_code)
previous_names = names_for_locale(row.attributes['prev_names'], language_code)
cur_aliases = current_names.reject { _1['primary_flg'] }.map { _1['name'] }
prev_aliases = previous_names.reject { _1['primary_flg'] }.map { _1['name'] }
cur_aliases = split_values(row.aliases)
prev_aliases = split_values(row.attributes['prev_aliases'])
cur_parent_tag_ids = split_parent_tag_ids(row.parent_tag_ids)
prev_parent_tag_ids = split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])
@@ -51,23 +47,21 @@ class TagVersionsController < ApplicationController
tags_by_id =
Tag
.includes(:tag_names, :materials, tag_names: :wiki_page)
.includes(:tag_name, :materials, { tag_name: :wiki_page })
.where(id: all_parent_tag_ids)
.index_by(&:id)
parent_tags =
build_version_values(cur_parent_tag_ids, prev_parent_tag_ids, key: :tag_id)
.map do |h|
{ tag: TagRepr.base(tags_by_id[h[:tag_id]], language_code:),
{ tag: TagRepr.base(tags_by_id[h[:tag_id]]),
type: h[:type] }
end
{ tag_id: row.tag_id,
version_no: row.version_no,
event_type: row.event_type,
names: row.names,
name: { current: current_names.find { _1['primary_flg'] }&.fetch('name'),
prev: previous_names.find { _1['primary_flg'] }&.fetch('name') },
name: { current: row.name, prev: row.attributes['prev_name'] },
category: { current: row.category, prev: row.attributes['prev_category'] },
deprecated_at: { current: row.deprecated_at&.iso8601,
prev: row.attributes['prev_deprecated_at']&.iso8601 },
@@ -97,12 +91,5 @@ class TagVersionsController < ApplicationController
def split_values(values) = values.to_s.split(/\s+/).reject(&:blank?)
def names_for_locale names, language_code
values = names.is_a?(String) ? JSON.parse(names) : Array(names)
values.select { _1['language_code'] == language_code }
rescue JSON::ParserError
[]
end
def split_parent_tag_ids(values) = split_values(values).map(&:to_i)
end
+111 -199
ファイルの表示
@@ -5,8 +5,6 @@ require 'set'
class TagsController < ApplicationController
def index
locale = resolve_locale!
post_id = params[:post]
name = params[:name].presence
@@ -38,14 +36,9 @@ class TagsController < ApplicationController
tags =
if post_id.present?
Tag
.joins(:posts, :tag_names)
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
posts: { id: post_id })
Tag.joins(:posts, :tag_name).where(posts: { id: post_id })
else
Tag
.joins(:tag_names)
.where(tag_names: { language_code: locale.language_code, primary_flg: true })
Tag.joins(:tag_name)
end
external_tags =
@@ -174,10 +167,9 @@ class TagsController < ApplicationController
tags_by_id =
Tag
.joins(:tag_names)
.includes(:tag_names, :materials, tag_names: :wiki_page)
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
id: tag_ids)
.joins(:tag_name)
.includes(:tag_name, :materials, tag_name: :wiki_page)
.where(id: tag_ids)
.index_by(&:id)
external_tags_by_id =
ExternalTag
@@ -186,7 +178,7 @@ class TagsController < ApplicationController
render json: { tags: rows.map { |row|
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
external_tag_json(external_tags_by_id.fetch(row['id']))
end
@@ -198,8 +190,7 @@ class TagsController < ApplicationController
parent_tag_id = nil if parent_tag_id <= 0
material_filter = material_filter_param(default: 'any')
locale = resolve_locale!
graph = build_with_depth_graph(material_filter, locale.language_code)
graph = build_with_depth_graph(material_filter)
tag_ids =
if parent_tag_id
@@ -214,8 +205,6 @@ class TagsController < ApplicationController
end
def autocomplete
locale = resolve_locale!
q = params[:q].to_s.strip.sub(/\Anot:/i, '')
prefix = "#{ ActiveRecord::Base.sanitize_sql_like(q) }%"
@@ -224,36 +213,35 @@ class TagsController < ApplicationController
alias_rows =
TagName
.where(language_code: locale.language_code, primary_flg: false)
.where('name LIKE ?', prefix)
.pluck(:tag_id, :name)
.where.not(canonical_id: nil)
.pluck(:canonical_id, :name)
matched_alias_by_tag_id = { }
tag_ids = []
alias_rows.each do |tag_id, alias_name|
tag_ids << tag_id
matched_alias_by_tag_id[tag_id] ||= alias_name
matched_alias_by_tag_name_id = { }
canonical_ids = []
alias_rows.each do |canonical_id, alias_name|
canonical_ids << canonical_id
matched_alias_by_tag_name_id[canonical_id] ||= alias_name
end
base =
Tag
.joins(:tag_names)
.includes(:tag_names, :materials, tag_names: :wiki_page)
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
deprecated_at: nil)
.joins(:tag_name)
.includes(:tag_name, :materials, tag_name: :wiki_page)
.where(deprecated_at: nil)
base = base.where('tags.post_count > 0') if present_only
canonical_hit = base.where('tag_names.name LIKE ?', prefix)
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_tags
.order(Arel.sql('tags.post_count DESC, tag_names.name'))
.limit(20)
.map { |tag|
TagRepr.base(tag, language_code: locale.language_code)
.merge(matched_alias: matched_alias_by_tag_id[tag.id])
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
}
return render json: internal_rows unless with_nico
@@ -270,22 +258,19 @@ class TagsController < ApplicationController
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)
render json: rows
end
def show
locale = resolve_locale!
tag =
Tag
.joins(:tag_names)
.includes(:tag_names, :materials, tag_names: :wiki_page)
.find_by(id: params[:id],
tag_names: { language_code: locale.language_code, primary_flg: true })
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
.joins(:tag_name)
.includes(:tag_name, :materials, tag_name: :wiki_page)
.find_by(id: params[:id])
return render json: TagRepr.base(tag) if tag
external_tag = ExternalTag.find_by(id: params[:id])
return render json: ExternalTagRepr.base(external_tag) if external_tag
@@ -297,14 +282,12 @@ class TagsController < ApplicationController
name = params[:name].to_s.strip
return render_bad_request('name は必須です.') if name.blank?
locale = resolve_locale!
tag =
Tag
.joins(:tag_names)
.includes(:tag_names, :materials, tag_names: :wiki_page)
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
.joins(:tag_name)
.includes(:tag_name, :materials, tag_name: :wiki_page)
.find_by(tag_names: { name: })
return render json: TagRepr.base(tag) if tag
platform, external_name = name.split(':', 2)
return head :not_found unless external_name
@@ -316,52 +299,35 @@ class TagsController < ApplicationController
end
def deerjikists
locale = resolve_locale!
tag =
Tag.joins(:tag_names)
.includes(:tag_names, tag_names: :wiki_page)
.find_by(id: params[:id],
tag_names: { language_code: locale.language_code, primary_flg: true })
tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(id: params[:id])
return head :not_found unless tag
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
deerjikists: DeerjikistRepr.many(
tag.deerjikists,
language_code: locale.language_code) }
render json: { tag: TagRepr.base(tag),
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
end
def deerjikists_by_name
name = params[:name].to_s.strip
return render_bad_request('name は必須です.') if name.blank?
locale = resolve_locale!
tag =
Tag
.joins(:tag_names)
.includes(:tag_names, tag_names: :wiki_page)
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(tag_names: { name: })
return head :not_found unless tag
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
deerjikists: DeerjikistRepr.many(
tag.deerjikists,
language_code: locale.language_code) }
render json: { tag: TagRepr.base(tag),
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
end
def update_deerjikists
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
tag =
Tag
.joins(:tag_names)
.includes(:tag_names, tag_names: :wiki_page)
.find_by(id: params[:id],
tag_names: { language_code: locale.language_code, primary_flg: true })
tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(id: params[:id])
return head :not_found unless tag
rows = normalise_deerjikist_rows(tag)
@@ -380,7 +346,7 @@ class TagsController < ApplicationController
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?
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
rescue ActiveRecord::RecordNotUnique
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
raise ActiveRecord::Rollback if performed?
end
@@ -409,28 +375,22 @@ class TagsController < ApplicationController
return if performed?
render json: DeerjikistRepr.many(tag.reload.deerjikists,
language_code: locale.language_code)
render json: DeerjikistRepr.many(tag.reload.deerjikists)
end
def materials_by_name
name = params[:name].to_s.strip
return render_bad_request('name は必須です.') if name.blank?
locale = resolve_locale!
material_filter = material_filter_param(default: 'any')
tag =
Tag
.joins(:tag_names)
.includes(:tag_names, :materials, tag_names: :wiki_page)
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
tag = Tag.joins(:tag_name)
.includes(:tag_name, :materials, tag_name: :wiki_page)
.find_by(tag_names: { name: })
return head :not_found unless tag
graph = build_with_depth_graph(material_filter, locale.language_code)
graph = build_with_depth_graph(material_filter)
render json: build_tag_children(tag, graph:, language_code: locale.language_code)
render json: build_tag_children(tag, graph:)
end
def update_all
@@ -446,9 +406,7 @@ class TagsController < ApplicationController
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
return render_unprocessable_entity '廃止状態は必須です.', field: :deprecated unless params.key?(:deprecated)
locale = resolve_locale!
return unless validate_tag_rename(locale, tag, name)
return unless validate_tag_rename(tag, name)
alias_names = params[:aliases].to_s.split.uniq
parent_names = params[:parent_tags].to_s.split.uniq
@@ -459,25 +417,24 @@ class TagsController < ApplicationController
end
ApplicationRecord.transaction do
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
language_code: locale.language_code)
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
old_name = tag.name(locale.language_code)
old_name = tag.name
name_changed = name != old_name
wiki_page = tag.tag_name(locale.language_code)&.wiki_page if name_changed
wiki_page = tag.tag_name.wiki_page if name_changed
if tag.deprecated? == deprecated
tag.update!(category:)
else
tag.update!(category:, deprecated_at: deprecated ? Time.current : nil)
end
rename_tag_name!(locale, tag, name) if name_changed
rename_tag_name!(tag, name) if name_changed
alias_names << old_name if name_changed && old_name.present?
alias_names << old_name if name_changed
alias_names.delete(name)
update_aliases!(locale, tag, alias_names)
update_parent_tags!(locale, tag, parent_names)
update_aliases!(tag, alias_names)
update_parent_tags!(tag, parent_names)
tag.reload
@@ -485,20 +442,17 @@ class TagsController < ApplicationController
tag,
event_type: :update,
created_by_user: current_user,
language_code: locale.language_code,
name_changed:,
wiki_page:)
end
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
render json: TagRepr.base(tag.reload)
end
def update
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
name = params[:name].presence
category = params[:category].presence
deprecated_given = params.key?(:deprecated)
@@ -506,21 +460,20 @@ class TagsController < ApplicationController
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'
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
end
ApplicationRecord.transaction do
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
language_code: locale.language_code)
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
old_name = tag.name(locale.language_code)
old_name = tag.name
name_changed = name.present? && name != old_name
wiki_page = tag.tag_name(locale.language_code)&.wiki_page if name_changed
wiki_page = tag.tag_name.wiki_page if name_changed
rename_tag_name!(locale, tag, name) if name_changed
rename_tag_name!(tag, name) if name_changed
tag.update!(category:) if category.present?
if deprecated_given && tag.deprecated? != deprecated
tag.update!(deprecated_at: deprecated ? Time.current : nil)
@@ -532,12 +485,11 @@ class TagsController < ApplicationController
tag,
event_type: :update,
created_by_user: current_user,
language_code: locale.language_code,
name_changed:,
wiki_page:)
end
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
render json: TagRepr.base(tag.reload)
end
private
@@ -549,7 +501,7 @@ class TagsController < ApplicationController
value
end
def build_with_depth_graph material_filter, language_code
def build_with_depth_graph material_filter
children_by_parent_id = Hash.new { |h, k| h[k] = [] }
parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] }
@@ -564,11 +516,8 @@ class TagsController < ApplicationController
material_tag_ids = Material.unscoped.kept.where.not(tag_id: nil).distinct.pluck(:tag_id).to_set
tags_by_id =
Tag
.joins(:tag_names)
.where(id: tag_ids,
tag_names: { language_code:, primary_flg: true })
tags_by_id = Tag.joins(:tag_name)
.where(id: tag_ids)
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
.each_with_object({ }) do |(id, name, category, deprecated_at), h|
h[id] = { name:, category:, deprecated: deprecated_at.present?,
@@ -702,33 +651,29 @@ class TagsController < ApplicationController
children: [] }
end
def build_tag_children tag, graph: nil, language_code: 'ja'
def build_tag_children tag, graph: nil
material = tag.materials.first
tag_graph = graph && graph[:tags_by_id][tag.id]
material =
nil if tag_graph && !with_depth_visible_tag?(tag_graph, graph[:material_filter])
children = tag.children.sort_by { _1.name(language_code) }
children = tag.children.sort_by(&:name)
if graph
children = children.filter { |child_tag| visible_subtree?(child_tag.id, graph) }
end
TagRepr.base(tag, language_code:).merge(
children: children.map { build_tag_children(_1, graph:, language_code:) },
TagRepr.base(tag).merge(
children: children.map { build_tag_children(_1, graph:) },
has_material: tag_graph ? tag_graph[:has_material] : material.present?,
material: material && MaterialRepr.base(
material,
host: request.base_url,
language_code:))
material: material && MaterialRepr.base(material, host: request.base_url))
end
def record_tag_version! tag, event_type:, created_by_user:, language_code: 'ja',
name_changed: false, wiki_page: nil
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
return unless name_changed
wiki_page ||= tag.tag_name('ja')&.wiki_page
wiki_page ||= tag.tag_name.wiki_page
return unless wiki_page&.wiki_versions&.exists?
WikiVersionRecorder.record!(
@@ -737,43 +682,30 @@ class TagsController < ApplicationController
created_by_user:)
end
def validate_tag_rename locale, tag, name
return true if name.blank? || name == tag.name(locale.language_code)
def validate_tag_rename tag, name
return true if name.blank? || name == tag.name
system_tag_names = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ']
if TagName.exists?(tag_id: tag.id,
language_code: 'ja',
primary_flg: true,
name: system_tag_names)
if tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico])
render_unprocessable_entity 'システム・タグの名称は変更できません.', field: :name
return false
end
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
return true unless target_tag_name&.primary_flg
target_tag_name = TagName.find_by(name:)
return true if target_tag_name.nil?
return true if target_tag_name.canonical_id?
render_unprocessable_entity 'その名前は既に使はれてゐます.', field: :name
false
end
def rename_tag_name! locale, tag, name
return if name == tag.name(locale.language_code)
def rename_tag_name! tag, name
return if name == tag.name
current_tag_name = tag.tag_name(locale.language_code)
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
current_tag_name = tag.tag_name
target_tag_name = TagName.find_by(name:)
unless target_tag_name
if current_tag_name
if target_tag_name.nil?
current_tag_name.update!(name:)
else
current_tag_name = TagName.create!(
tag:,
language_code: locale.language_code,
script_code: locale.script_code,
name:,
primary_flg: true)
tag.association(:tag_names).reset
end
return
end
@@ -781,89 +713,71 @@ class TagsController < ApplicationController
end
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
old_owner_tag = promoted_tag_name.tag
old_owner_tag = promoted_tag_name.canonical&.tag
if old_owner_tag && old_owner_tag != tag
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
end
current_tag_name.wiki_page&.update!(tag_name_id: promoted_tag_name.id)
current_tag_name.association(:wiki_page).reset
promoted_tag_name.update!(canonical: nil)
current_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
promoted_tag_name.update!(tag:, canonical_id: nil, primary_flg: true)
TagName.where(tag:,
language_code: current_tag_name.language_code,
primary_flg: false)
TagName.where(canonical_id: current_tag_name.id)
.where.not(id: promoted_tag_name.id)
.find_each do |alias_tag_name|
alias_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
alias_tag_name.update!(canonical: promoted_tag_name)
end
tag.update!(tag_name_id: promoted_tag_name.id)
current_tag_name.wiki_page&.update!(tag_name: promoted_tag_name)
tag.update!(tag_name: promoted_tag_name)
current_tag_name.association(:wiki_page).reset
current_tag_name.association(:tag).reset
current_tag_name.reload.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
current_tag_name.reload.update!(canonical: promoted_tag_name)
return unless old_owner_tag && old_owner_tag != tag
record_tag_version!(old_owner_tag.reload, event_type: :update,
created_by_user: current_user,
language_code: current_tag_name.language_code)
record_tag_version!(old_owner_tag.reload, event_type: :update, created_by_user: current_user)
end
def update_aliases! locale, tag, alias_names
def update_aliases! tag, alias_names
alias_names = alias_names.uniq
affected_tags = [tag]
primary_tag_name = TagName.find_by!(tag:,
language_code: locale.language_code,
primary_flg: true)
current_aliases = tag.tag_names.where(language_code: locale.language_code,
primary_flg: false).to_a
current_aliases = tag.tag_name.aliases.to_a
current_aliases.each do |alias_tag_name|
next if alias_names.include?(alias_tag_name.name)
affected_tags << alias_tag_name.tag
affected_tags << alias_tag_name.canonical&.tag
end
alias_names.each do |alias_name|
alias_tag_name = TagName.find_by(language_code: locale.language_code, name: alias_name)
affected_tags << alias_tag_name&.tag
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
affected_tags << alias_tag_name.canonical&.tag
end
affected_tags.compact.uniq.each do |affected_tag|
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user,
language_code: locale.language_code)
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user)
end
current_aliases.each do |alias_tag_name|
next if alias_names.include?(alias_tag_name.name)
alias_tag_name.update!(canonical_id: nil, tag_id: nil)
alias_tag_name.update!(canonical: nil)
end
alias_names.each do |alias_name|
alias_tag_name = TagName.find_or_initialize_by(
language_code: locale.language_code,
name: alias_name)
alias_tag_name.script_code = locale.script_code
alias_tag_name.update!(tag:, primary_flg: false, canonical_id: primary_tag_name.id)
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
alias_tag_name.update!(canonical: tag.tag_name)
end
affected_tags.compact.uniq.each do |affected_tag|
record_tag_version!(affected_tag, event_type: :update,
created_by_user: current_user,
language_code: locale.language_code)
record_tag_version!(affected_tag, event_type: :update, created_by_user: current_user)
end
end
def update_parent_tags! locale, tag, parent_names
parent_tags = Tag.normalise_tags!(locale, parent_names,
with_tagme: false,
def update_parent_tags! tag, parent_names
parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false,
with_no_deerjikist: false)
old_parent_tags = tag.parents.to_a
@@ -937,15 +851,13 @@ class TagsController < ApplicationController
.to_a
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 }
return if conflicts.empty?
tag_names_by_id =
Tag
.joins(:tag_names)
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
id: conflicts.map(&:tag_id).uniq)
tag_names_by_id = Tag
.joins(:tag_name)
.where(id: conflicts.map(&:tag_id).uniq)
.pluck('tags.id', 'tag_names.name')
.to_h
fields = { }
+2 -2
ファイルの表示
@@ -6,7 +6,7 @@ class TheatreSkipEventsController < ApplicationController
events =
TheatreSkipEvent
.where(theatre_id: params[:theatre_id])
.includes(:post, tags: :tag_names)
.includes(:post, tags: :tag_name)
.order(created_at: :desc)
.limit(limit)
@@ -14,7 +14,7 @@ class TheatreSkipEventsController < ApplicationController
{ id: event.id,
theatre_id: event.theatre_id,
post: { id: event.post.id, title: event.post.title, url: event.post.url },
tags: event.tags.map { |tag| { id: tag.id, name: tag.name('ja') } },
tags: event.tags.map { |tag| { id: tag.id, name: tag.name } },
programme_position: event.programme_position,
created_at: event.created_at }
}
+1 -5
ファイルの表示
@@ -94,11 +94,7 @@ class WikiPagesController < ApplicationController
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
tag_name = TagName.find_or_create_by!(language_code: 'ja', name: title) do |name|
name.script_code = 'Jpan'
name.primary_flg = true
name.auto_generated = false
end
tag_name = TagName.find_or_create_by!(name: title)
page =
Wiki::Commit.create_content!(
+2 -6
ファイルの表示
@@ -14,11 +14,7 @@ class ExternalTag < ApplicationRecord
has_many :linked_tags, through: :nico_tag_relations, source: :tag
def snapshot_linked_tag_names language_code = 'ja'
linked_tags
.joins(:tag_names)
.where(tag_names: { language_code:, primary_flg: true })
.order('tag_names.name')
.pluck('tag_names.name')
def snapshot_linked_tag_names
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
end
end
-3
ファイルの表示
@@ -1,3 +0,0 @@
class Language < ApplicationRecord
has_many :locales, foreign_key: :language_code, primary_key: :code
end
-24
ファイルの表示
@@ -1,24 +0,0 @@
class Locale < ApplicationRecord
after_create_commit :generate_tag_names!
belongs_to :language, foreign_key: :language_code, primary_key: :code
belongs_to :script, foreign_key: :script_code, primary_key: :code
def self.nipponese = Locale.find('ja')
private
def generate_tag_names!
tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id)
Tag.where.not(id: tag_ids).find_each do
TagName.create!(tag_id: _1.id,
language_code:,
name: TagName.generate_name(self, _1, _1.name('ja')),
script_code:,
primary_flg: true,
# TODO: 公証実装したら書く.
# auto_generated: true,
canonical_id: nil)
end
end
end
+8 -10
ファイルの表示
@@ -126,14 +126,13 @@ class Post < ApplicationRecord
super(options).merge(thumbnail: nil)
end
def snapshot_tag_names language_code = 'ja'
def snapshot_tag_names
post_tags
.joins(tag: :tag_names)
.where(tag_names: { language_code:, primary_flg: true })
.includes(:sections, tag: :tag_names)
.joins(tag: :tag_name)
.includes(:sections, tag: :tag_name)
.order('tag_names.name')
.map do |post_tag|
name = post_tag.tag.name(language_code)
name = post_tag.tag.tag_name.name
sections = post_tag.sections.sort_by(&:begin_ms)
next name if sections.empty?
@@ -153,17 +152,16 @@ class Post < ApplicationRecord
"#{ tag.fetch('name') }#{ sections.join }"
end
def snapshot_tags_json language_code = 'ja'
def snapshot_tags_json
tag_snapshots =
post_tags
.joins(tag: :tag_names)
.where(tag_names: { language_code:, primary_flg: true })
.includes(:sections, tag: :tag_names)
.joins(tag: :tag_name)
.includes(:sections, tag: :tag_name)
.order('tags.id')
.map { |pt|
{ 'tag_id' => pt.tag.id,
'version_no' => pt.tag.version_no,
'name' => pt.tag.name(language_code),
'name' => pt.tag.name,
'category' => pt.tag.category,
'sections' => pt.sections.sort_by(&:begin_ms).map {
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
-3
ファイルの表示
@@ -1,3 +0,0 @@
class Script < ApplicationRecord
;
end
+36 -59
ファイルの表示
@@ -50,10 +50,14 @@ class Tag < ApplicationRecord
has_many :tag_versions
has_many :tag_names
belongs_to :tag_name
delegate :wiki_page, to: :tag_name
attribute :version_no, :integer, default: 1
delegate :name, to: :tag_name, allow_nil: true
validates :tag_name, presence: true
enum :category, deerjikist: 'deerjikist',
meme: 'meme',
character: 'character',
@@ -63,8 +67,9 @@ class Tag < ApplicationRecord
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 :tag_names_must_be_valid
CATEGORY_PREFIXES = {
'general:' => :general,
@@ -78,20 +83,8 @@ class Tag < ApplicationRecord
'mtr:' => :material,
'meta:' => :meta }.freeze
SYSTEM_TAG_NAMES = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ', 'YouTube'].freeze
def tag_name language_code
if tag_names.loaded?
tag_names.find { _1.language_code == language_code && _1.primary_flg }
else
tag_names.find_by(language_code:, primary_flg: true)
end
end
def name(language_code) = tag_name(language_code)&.name
def wiki_page = tag_name('ja')&.wiki_page
def system?
tag_name('ja')&.name.in?(SYSTEM_TAG_NAMES)
def name= val
(self.tag_name ||= build_tag_name).name = val
end
def deprecated? = deprecated_at?
@@ -102,16 +95,14 @@ class Tag < ApplicationRecord
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
def self.tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta)
def self.bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
def self.no_deerjikist =
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
def self.niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
def self.youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
def self.video = find_or_create_by_tag_name!('動画', category: :meta)
def self.niconico = find_or_create_by_tag_name!('ニコニコ', category: :meta)
def self.youtube = find_or_create_by_tag_name!('YouTube', category: :meta)
def self.normalise_tags! locale, tag_names,
with_tagme: true,
def self.normalise_tags! tag_names, with_tagme: true,
with_no_deerjikist: true,
deny_deprecated: false,
with_sections: false
@@ -141,11 +132,11 @@ class Tag < ApplicationRecord
raise SectionLiteralParseError.new(raw_name, raw_name)
end
name = TagName.canonicalise(locale, name).first
name = TagName.canonicalise(name).first
find_or_create_by_tag_name!(locale, name, category: (cat || :general)).tap do |tag|
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
if deny_deprecated && tag.deprecated?
raise DeprecatedTagNormalisationError, [tag.name(locale.language_code)]
raise DeprecatedTagNormalisationError, [tag.name]
end
tag.update!(category: cat) if cat && tag.category != cat
@@ -231,23 +222,13 @@ class Tag < ApplicationRecord
[left_end_ms, right_end_ms].max
end
def self.find_or_create_by_tag_name! locale, name, category:
language_code = locale.language_code
name = name.to_s.strip
def self.find_or_create_by_tag_name! name, category:
tn = TagName.find_or_create_by!(name: name.to_s.strip)
tn = tn.canonical if tn.canonical_id?
tn = TagName.find_or_create_by!(language_code:, name:) do
_1.script_code = locale.script_code
_1.primary_flg = true
Tag.find_or_create_by!(tag_name_id: tn.id) do |t|
t.category = category
end
tag = tn.tag
return tag if tag
tag = Tag.create!(tag_name_id: tn.id, category:)
tn.update!(tag:)
tag.association(:tag_names).reset
tag
rescue ActiveRecord::RecordNotUnique
retry
end
@@ -276,15 +257,13 @@ class Tag < ApplicationRecord
end
end
source_tag_name = source_tag.tag_name('ja')
source_tag_name = source_tag.tag_name
if source_tag_name.wiki_page.present?
raise ActiveRecord::RecordInvalid.new(source_tag_name)
end
TagVersioning.record!(source_tag, event_type: :discard, created_by_user:)
source_tag.tag_names.update_all(tag_id: target_tag.id, primary_flg: false,
updated_at: Time.current)
source_tag.destroy!
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
@@ -305,27 +284,25 @@ class Tag < ApplicationRecord
target_tag.reload
end
def snapshot_aliases(language_code)
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
end
def snapshot_aliases = tag_name.aliases.order(:name).pluck(:name)
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
def snapshot_linked_tag_names language_code
linked_tags
.joins(:tag_names)
.where(tag_names: { language_code:, primary_flg: true })
.order('tag_names.name')
.pluck('tag_names.name')
def snapshot_linked_tag_names
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
end
private
def tag_names_must_be_valid
tag_names.each do |tag_name|
next if tag_name.valid?
def tag_name_mustnt_start_with_nico
if name.to_s.downcase.start_with?('nico:')
errors.add :name, 'タグの命名規則に反してゐます.'
end
end
tag_name.errors[:name].each { errors.add :name, _1 }
def tag_name_must_be_canonical
if tag_name&.canonical_id?
errors.add :tag_name, 'tag_names へは実体を示す必要があります.'
end
end
+20 -37
ファイルの表示
@@ -1,54 +1,43 @@
class TagName < ApplicationRecord
belongs_to :language, foreign_key: :language_code, primary_key: :code
belongs_to :tag, optional: true
has_one :tag
has_one :wiki_page
validates :name, presence: true,
length: { maximum: 255 },
uniqueness: { scope: :language_code }
belongs_to :canonical, class_name: 'TagName', optional: true
has_many :aliases, class_name: 'TagName', foreign_key: :canonical_id
validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
validate :canonical_must_be_canonical
validate :alias_name_must_not_have_prefix
validate :alias_must_not_have_wiki_page
validate :canonical_must_not_be_present_with_tag_or_wiki_page
validate :name_must_be_sanitised
validate :name_mustnt_start_with_nico
def primary? = primary_flg
def self.canonicalise locale, names
def self.canonicalise names
names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?)
return [] if names.blank?
tns = TagName.where(language_code: locale.language_code, name: names).index_by(&:name)
tns = TagName.includes(:canonical).where(name: names).index_by(&:name)
names.map { |name|
tag_name = tns[name]
if !tag_name || tag_name.primary?
name
else
TagName.find_by(language_code: locale.language_code,
tag_id: tag_name.tag_id,
primary_flg: true)&.name || name
end
}.uniq
end
def self.generate_name locale, tag, name
# TODO: 言語ごとの自動命名ロジック完成したら書く.
"Tag_##{ tag.id }"
names.map { |name| tns[name]&.canonical&.name || name }.uniq
end
private
def canonical_must_be_canonical
if canonical&.canonical_id?
errors.add :canonical, 'canonical は実体を示す必要があります.'
end
end
def alias_name_must_not_have_prefix
if !(primary?) && name.to_s.include?(':')
if canonical_id? && name.to_s.include?(':')
errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.'
end
end
def alias_must_not_have_wiki_page
if !(primary?) && wiki_page
errors.add :primary_flg, 'Wiki 参照がある名前はエーリアスになれません.'
def canonical_must_not_be_present_with_tag_or_wiki_page
if canonical_id? && (tag || wiki_page)
errors.add :canonical, 'タグもしくは Wiki の参照がある名前はエーリアスになれません.'
end
end
@@ -57,10 +46,4 @@ class TagName < ApplicationRecord
errors.add :name, '名前に使用できない文字が含まれてゐます.'
end
end
def name_mustnt_start_with_nico
if name.to_s.downcase.start_with?('nico:')
errors.add :name, 'タグの命名規則に反してゐます.'
end
end
end
+5 -13
ファイルの表示
@@ -15,30 +15,23 @@ class TagNameSanitisationRule < ApplicationRecord
end
def apply!
Language.find_each do |language|
TagName.where(language:).find_each do |tn|
TagName.find_each do |tn|
name = sanitise(tn.name)
next if name == tn.name
TagName.transaction do
existing_tn = TagName.find_by(language:, name:)
existing_tn = TagName.find_by(name:)
if existing_tn
unless existing_tn.primary_flg
existing_tn = TagName.find_by!(language:,
tag_id: existing_tn.tag_id,
primary_flg: true)
end
existing_tn = existing_tn.canonical || existing_tn
next if existing_tn.id == tn.id
existing_tag = existing_tn.tag
source_tag = tn.tag
existing_tag = Tag.find_by(tag_name_id: existing_tn.id)
source_tag = Tag.find_by(tag_name_id: tn.id)
if existing_tag
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
elsif source_tag
tn.update_columns(tag_id: nil, updated_at: Time.current)
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
existing_tn.update_columns(tag_id: source_tag.id, updated_at: Time.current)
end
tn.destroy!
@@ -50,7 +43,6 @@ class TagNameSanitisationRule < ApplicationRecord
end
end
end
end
private
-1
ファイルの表示
@@ -11,6 +11,5 @@ class TagVersion < ApplicationRecord
meta: 'meta' }, validate: true
validates :name, presence: true
validates :names, presence: true
validates :category, presence: true
end
+4 -5
ファイルの表示
@@ -6,12 +6,11 @@ module DeerjikistRepr
module_function
def base deerjikist, language_code: 'ja'
deerjikist.as_json(BASE).merge(
tag: deerjikist.tag && TagRepr.base(deerjikist.tag, language_code:))
def base deerjikist
deerjikist.as_json(BASE)
end
def many deerjikists, language_code: 'ja'
deerjikists.map { |d| base(d, language_code:) }
def many deerjikists
deerjikists.map { |d| base(d) }
end
end
+13 -14
ファイルの表示
@@ -12,15 +12,14 @@ module MaterialRepr
module_function
def base material, host:, language_code: 'ja'
def base material, host:
material.as_json(BASE).merge(
tag: material.tag && TagRepr.base(material.tag, language_code:),
file: if material.file.attached?
Rails.application.routes.url_helpers.rails_storage_proxy_url(
material.file, host:)
end,
thumbnail: thumbnail_url(material, host:),
thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:),
thumbnail_fallback_text: thumbnail_fallback_text(material),
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
media_kind: media_kind(material),
file_byte_size: material.file_byte_size,
@@ -28,11 +27,11 @@ module MaterialRepr
export_items: export_items(material))
end
def many materials, host:, language_code: 'ja'
materials.map { |m| base(m, host:, language_code:) }
def many materials, host:
materials.map { |m| base(m, host:) }
end
def list material, host:, language_code: 'ja'
def list material, host:
{ id: material.id,
version_no: material.version_no,
url: material.url,
@@ -41,9 +40,9 @@ module MaterialRepr
source_path: material.source_path,
source_file_id: material.source_file_id,
normalized_source_key: material.normalized_source_key,
tag: compact_tag(material.tag, language_code:),
tag: compact_tag(material.tag),
thumbnail: thumbnail_url(material, host:),
thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:),
thumbnail_fallback_text: thumbnail_fallback_text(material),
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
media_kind: media_kind(material),
content_type: material.content_type,
@@ -54,8 +53,8 @@ module MaterialRepr
export_items: export_items(material) }
end
def list_many materials, host:, language_code: 'ja'
materials.map { |m| list(m, host:, language_code:) }
def list_many materials, host:
materials.map { |m| list(m, host:) }
end
def export_paths material
@@ -80,8 +79,8 @@ module MaterialRepr
material.thumbnail, host:)
end
def thumbnail_fallback_text material, language_code:
material.tag&.name(language_code) || material.created_at&.strftime('%Y-%m-%d')
def thumbnail_fallback_text material
material.tag&.name || material.created_at&.strftime('%Y-%m-%d')
end
def thumbnail_fallback_kind material
@@ -99,11 +98,11 @@ module MaterialRepr
'file_other'
end
def compact_tag tag, language_code:
def compact_tag tag
return nil unless tag
{ id: tag.id,
name: tag.name(language_code),
name: tag.name,
category: tag.category,
deprecated_at: tag.deprecated_at }
end
+8 -8
ファイルの表示
@@ -17,23 +17,23 @@ module PostRepr
module_function
def base post, current_user = nil, host: nil, language_code: 'ja'
def base post, current_user = nil, host: nil
json =
if host.present?
common(post, host:)
else
common(post)
end
json['tags'] = tag_json(post, language_code:)
json['tags'] = tag_json(post)
json['uploaded_user'] = post.uploaded_user && UserRepr.base(post.uploaded_user)
json['viewed'] = current_user ? current_user.viewed?(post) : false
json
end
def detail post, current_user = nil, parent_posts: [], child_posts: [],
sibling_posts: { }, related: [], host: nil, language_code: 'ja'
sibling_posts: { }, related: [], host: nil
if host.present?
base(post, current_user, host:, language_code:).merge(
base(post, current_user, host:).merge(
'parent_posts' => cards(parent_posts, host:),
'child_posts' => cards(child_posts, host:),
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
@@ -41,7 +41,7 @@ module PostRepr
},
'related' => cards(related, host:))
else
base(post, current_user, language_code:).merge(
base(post, current_user).merge(
'parent_posts' => cards(parent_posts),
'child_posts' => cards(child_posts),
'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts|
@@ -86,14 +86,14 @@ module PostRepr
end)
end
def tag_json post, language_code:
def tag_json post
internal_tags =
post
.post_tags
.reject { _1.tag.deprecated? }
.sort_by { TagRepr.name_for(_1.tag, language_code) }
.sort_by { _1.tag.name }
.map { |post_tag|
TagRepr.inline(post_tag.tag, language_code:).merge(
TagRepr.inline(post_tag.tag).merge(
'children' => [],
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
}
+7 -12
ファイルの表示
@@ -3,23 +3,18 @@
module TagRepr
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at, :deprecated_at],
methods: [:has_wiki, :material_id, :has_deerjikists] }.freeze
methods: [:name, :has_wiki, :material_id, :has_deerjikists] }.freeze
module_function
def base tag, language_code: 'ja'
tag.as_json(BASE).merge(name: name_for(tag, language_code),
aliases: tag.snapshot_aliases(language_code),
parents: tag.parents.map { inline(_1, language_code:) })
def base tag
tag.as_json(BASE).merge(aliases: tag.snapshot_aliases,
parents: tag.parents.map { _1.as_json(BASE) })
end
def inline tag, language_code: 'ja'
tag.as_json(BASE).merge(name: name_for(tag, language_code), aliases: [], parents: [])
def inline tag
tag.as_json(BASE).merge(aliases: [], parents: [])
end
def many(tags, language_code: 'ja') = tags.map { |tag| base(tag, language_code:) }
def name_for tag, language_code
tag.name(language_code) || (tag.system? ? tag.name('ja') : nil)
end
def many(tags) = tags.map { |t| base(t) }
end
+1 -1
ファイルの表示
@@ -31,7 +31,7 @@ class MaterialVersionRecorder < VersionRecorder
{ url: @record.url,
parent: @record.parent,
tag: @record.tag,
tag_name: @record.tag&.name('ja'),
tag_name: @record.tag&.name,
tag_category: @record.tag&.category,
source_kind: @record.source_kind,
source_uri: @record.source_uri,
+5 -5
ファイルの表示
@@ -6,7 +6,7 @@ class PostBulkCreator
@host = host
end
def run locale
def run
results = Array.new(@posts.length)
mutex = Mutex.new
next_index = 0
@@ -27,7 +27,7 @@ class PostBulkCreator
break if index >= @posts.length
attributes = @posts[index]
results[index] = create_row(locale, actor, attributes, index)
results[index] = create_row(actor, attributes, index)
rescue StandardError => e
Rails.logger.error(
"post_bulk_creator_worker_failure #{ { error: e.class.name,
@@ -61,12 +61,12 @@ class PostBulkCreator
private
def create_row locale, actor, attributes, index
def create_row actor, attributes, index
preflight =
PostCreatePreflight.new(
attributes: attributes,
thumbnail: thumbnail_for(index, attributes),
host: @host).run(locale)
host: @host).run
if preflight[:existing_post_id].present?
return {
status: 'skipped',
@@ -76,7 +76,7 @@ class PostBulkCreator
post = PostCreator.new(
actor: actor,
attributes: normalised_attributes(attributes, preflight, index)).create!(locale)
attributes: normalised_attributes(attributes, preflight, index)).create!
result = {
status: 'created',
post: { id: post.id } }
+41 -52
ファイルの表示
@@ -8,13 +8,13 @@ class PostCreatePlan
@existing_tags_by_name = nil
end
def build! locale
direct_tag_specs, tag_sections = parse_direct_tag_specs(locale)
default_tag_specs = build_default_tag_specs(locale, direct_tag_specs)
def build!
direct_tag_specs, tag_sections = parse_direct_tag_specs
default_tag_specs = build_default_tag_specs(direct_tag_specs)
snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs)
preload_existing_tags_by_name!(locale, snapshot_tag_specs.map { _1[:name] })
validate_new_tag_specs!(locale, snapshot_tag_specs)
post_tag_specs = expand_parent_tag_specs(locale, snapshot_tag_specs)
preload_existing_tags_by_name!(snapshot_tag_specs.map { _1[:name] })
validate_new_tag_specs!(snapshot_tag_specs)
post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
video_ms = normalise_video_ms(snapshot_tag_specs)
validate_video_sections!(video_ms, tag_sections)
parent_post_ids = normalise_parent_post_ids
@@ -42,16 +42,14 @@ class PostCreatePlan
def tag_names = @attributes[:tags].to_s.split
def parse_direct_tag_specs locale
def parse_direct_tag_specs
tag_sections = { }
direct_tag_specs = []
tag_names.each do |raw_name|
tag_name, category, sections = parse_raw_tag_name(locale, raw_name)
existing_tag = existing_tags_by_name(locale)[tag_name]
if existing_tag&.deprecated?
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name(locale.language_code)]
end
tag_name, category, sections = parse_raw_tag_name(raw_name)
existing_tag = existing_tags_by_name[tag_name]
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
direct_tag_specs << {
name: tag_name,
@@ -67,7 +65,7 @@ class PostCreatePlan
[merge_tag_specs(direct_tag_specs), tag_sections]
end
def parse_raw_tag_name locale, raw_name
def parse_raw_tag_name raw_name
name = raw_name.to_s
prefix, category =
Tag::CATEGORY_PREFIXES.find {
@@ -76,7 +74,7 @@ class PostCreatePlan
name = name.sub(/\A#{ prefix }/i, '')
sections = []
while match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/)
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
name = "#{ match[1] }#{ match[4] }"
next if match[2].empty? && match[3].empty?
@@ -89,17 +87,17 @@ class PostCreatePlan
raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
end
[resolved_tag_name(locale, name), category&.to_sym, sections]
[resolved_tag_name(name), category&.to_sym, sections]
end
def build_default_tag_specs locale, direct_tag_specs
def build_default_tag_specs direct_tag_specs
default_tag_specs = []
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
default_tag_specs << {
name: TAGME_TAG_NAME,
category: :meta }
end
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
default_tag_specs << {
name: NO_DEERJIKIST_TAG_NAME,
category: :meta }
@@ -108,18 +106,17 @@ class PostCreatePlan
default_tag_specs
end
def validate_new_tag_specs! locale, specs
def validate_new_tag_specs! specs
Array(specs).each do |spec|
next if existing_tags_by_name(locale).key?(spec[:name])
next if existing_tags_by_name.key?(spec[:name])
validate_new_tag_spec!(locale, spec)
validate_new_tag_spec!(spec)
end
end
def validate_new_tag_spec! locale, spec
tag_name = TagName.new(name: spec[:name], language_code: locale.language_code,
script_code: locale.script_code, primary_flg: true)
tag = Tag.new(category: spec[:category])
def validate_new_tag_spec! spec
tag_name = TagName.new(name: spec[:name])
tag = Tag.new(category: spec[:category], tag_name:)
return if tag_name.valid? && tag.valid?
post = Post.new
@@ -134,15 +131,13 @@ class PostCreatePlan
raise ActiveRecord::RecordInvalid, post
end
def expand_parent_tag_specs locale, snapshot_tag_specs
existing_snapshot_tags = snapshot_tag_specs.filter_map do
existing_tags_by_name(locale)[_1[:name]]
end
def expand_parent_tag_specs snapshot_tag_specs
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
expanded_parent_specs =
Tag.expand_parent_tags(existing_snapshot_tags)
.reject(&:deprecated?)
.map { |tag|
{ name: tag.name(locale.language_code),
{ name: tag.name,
category: tag.category.to_sym }
}
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
@@ -161,37 +156,33 @@ class PostCreatePlan
}.values.sort_by { _1[:name] }
end
def existing_tags_by_name locale
def existing_tags_by_name
@existing_tags_by_name ||= begin
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
.includes(:tag)
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
end
end
def preload_existing_tags_by_name! locale, names
def preload_existing_tags_by_name! names
wanted_names = Array(names).map { _1.to_s }.reject(&:blank?).uniq
missing_names = wanted_names - existing_tags_by_name(locale).keys
missing_names = wanted_names - existing_tags_by_name.keys
return if missing_names.empty?
existing_tags_by_name(locale).merge!(
TagName.where(language_code: locale.language_code,
primary_flg: true,
name: missing_names)
.includes(:tag)
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag })
existing_tags_by_name.merge!(
Tag.joins(:tag_name)
.where(tag_names: { name: missing_names })
.index_by(&:name))
end
def canonical_tag_name_without_sections locale, raw_name
name, = parse_raw_tag_name(locale, raw_name)
def canonical_tag_name_without_sections raw_name
name, = parse_raw_tag_name(raw_name)
name
end
def deerjikist_tag_spec? locale, spec
def deerjikist_tag_spec? spec
return true if spec[:category] == :deerjikist
existing_tags_by_name(locale)[spec[:name]]&.deerjikist?
existing_tags_by_name[spec[:name]]&.deerjikist?
end
def normalise_parent_post_ids
@@ -269,12 +260,10 @@ class PostCreatePlan
end
end
def resolved_tag_name locale, name
tag_name = TagName.find_by(language_code: locale.language_code, name:)
return name if !(tag_name) || tag_name.primary?
def resolved_tag_name name
tag_name = TagName.includes(:canonical).find_by(name:)
return name if tag_name.nil?
TagName.find_by(language_code: locale.language_code,
tag_id: tag_name.tag_id,
primary_flg: true)&.name || name
(tag_name.canonical || tag_name).name
end
end
+2 -3
ファイルの表示
@@ -15,10 +15,9 @@ class PostCreatePreflight
@host = host
end
def run locale
def run
preview = PostImportPreviewer.new.preview_rows(
rows: [preview_row],
locale:,
fetch_metadata: false).first
if preview[:existing_post_id].present?
return {
@@ -54,7 +53,7 @@ class PostCreatePreflight
original_created_from: preview[:attributes]['original_created_from'],
original_created_before: preview[:attributes]['original_created_before'],
duration: preview[:attributes]['duration'],
video_ms: preview[:attributes]['video_ms'] }).build!(locale)
video_ms: preview[:attributes]['video_ms'] }).build!
{
url: plan[:url],
+29 -36
ファイルの表示
@@ -9,7 +9,7 @@ class PostCreator
@field_warnings = { }
end
def create! locale
def create!
thumbnail_attachment = prepare_thumbnail_attachment
post = Post.new(title: @attributes[:title].presence,
url: @attributes[:url],
@@ -21,17 +21,14 @@ class PostCreator
ApplicationRecord.transaction do
post.save!
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
snapshot_tags = planned_snapshot_tags(locale)
post_tags = planned_post_tags(locale)
sections = planned_sections(locale)
TagVersioning.record_tag_snapshots!(
snapshot_tags,
created_by_user: @actor,
language_code: locale.language_code)
post.video_ms = planned_video_ms(locale)
snapshot_tags = planned_snapshot_tags
post_tags = planned_post_tags
sections = planned_sections
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
post.video_ms = planned_video_ms
post.save!
sync_post_tags!(post, post_tags, sections)
sync_parent_posts!(post, planned_parent_post_ids(locale))
sync_parent_posts!(post, planned_parent_post_ids)
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
end
post
@@ -48,73 +45,69 @@ class PostCreator
thumbnail_base: @attributes[:thumbnail_base].presence)
end
def planned_snapshot_tags(locale) = planned_create_attributes(locale)[:snapshot_tags]
def planned_snapshot_tags = planned_create_attributes[:snapshot_tags]
def planned_post_tags(locale) = planned_create_attributes(locale)[:post_tags]
def planned_post_tags = planned_create_attributes[:post_tags]
def planned_sections(locale) = planned_create_attributes(locale)[:tag_sections]
def planned_sections = planned_create_attributes[:tag_sections]
def planned_parent_post_ids locale
planned_create_attributes(locale)[:normalised_parent_post_ids]
def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]
def planned_video_ms
planned_create_attributes[:video_ms]
end
def planned_video_ms locale
planned_create_attributes(locale)[:video_ms]
end
def planned_create_attributes locale
def planned_create_attributes
@planned_create_attributes ||= begin
if @attributes.key?(:snapshot_tag_specs)
snapshot_tags = materialise_tags(locale, @attributes[:snapshot_tag_specs] || [])
post_tags = materialise_tags(locale, @attributes[:post_tag_specs] || [])
snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
{
snapshot_tags: snapshot_tags,
post_tags: post_tags,
tag_sections: materialise_sections(
@attributes[:tag_sections] || { },
snapshot_tags,
post_tags,
locale),
post_tags),
normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
video_ms: @attributes[:video_ms] }
else
build_materialised_plan(locale)
build_materialised_plan
end
end
end
def build_materialised_plan locale
plan = PostCreatePlan.new(attributes: @attributes).build!(locale)
snapshot_tags = materialise_tags(locale, plan[:snapshot_tag_specs] || [])
post_tags = materialise_tags(locale, plan[:post_tag_specs] || [])
def build_materialised_plan
plan = PostCreatePlan.new(attributes: @attributes).build!
snapshot_tags = materialise_tags(plan[:snapshot_tag_specs] || [])
post_tags = materialise_tags(plan[:post_tag_specs] || [])
{
snapshot_tags: snapshot_tags,
post_tags: post_tags,
tag_sections: materialise_sections(
plan[:tag_sections] || { },
snapshot_tags,
post_tags,
locale),
post_tags),
normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
video_ms: plan[:video_ms] }
end
def materialise_tags locale, specs
def materialise_tags specs
Array(specs).each_with_object({ }) do |spec, tags|
name = spec[:name] || spec['name']
category = spec[:category] || spec['category']
next if name.blank? || category.blank?
tag = Tag.find_or_create_by_tag_name!(locale, name, category:)
tag = Tag.find_or_create_by_tag_name!(name, category:)
tag.update!(category:) if tag.category.to_sym != category.to_sym
tags[name] ||= tag
end.values
end
def materialise_sections sections_by_name, snapshot_tags, post_tags, locale = Locale.nipponese
tags_by_name = post_tags.index_by { _1.name(locale.language_code) }
def materialise_sections sections_by_name, snapshot_tags, post_tags
tags_by_name = post_tags.index_by(&:name)
snapshot_tags.each do |tag|
tags_by_name[tag.name(locale.language_code)] ||= tag
tags_by_name[tag.name] ||= tag
end
sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
+16 -20
ファイルの表示
@@ -16,7 +16,7 @@ class PostImportPreviewer
THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze
METADATA_FETCH_WARNING = '自動取得に失敗しました.'.freeze
def preview_rows rows:, locale: Locale.nipponese, fetch_metadata: true, metadata_cache: { }
def preview_rows rows:, fetch_metadata: true, metadata_cache: { }
prepared_rows = rows.map { prepare_row(_1) }
url_counts = prepared_rows.filter_map { _1[:normal_url] }.tally
existing_posts =
@@ -30,14 +30,13 @@ class PostImportPreviewer
url_counts)
known_tags =
preload_known_tags(
locale,
prepared_rows,
fetch_metadata,
metadata_cache,
existing_posts,
url_counts)
prepared_rows.map { |row|
preview_row(locale, row,
preview_row(row,
fetch_metadata:,
metadata_cache:,
existing_posts:,
@@ -60,7 +59,7 @@ class PostImportPreviewer
source.merge(url_text: url, normal_url:, url_error: validate_url_safety(normal_url))
end
def preview_row locale, row,
def preview_row row,
fetch_metadata:,
metadata_cache:,
existing_posts:,
@@ -125,8 +124,7 @@ class PostImportPreviewer
attributes['url'] = url
validate_basic_data(attributes, validation_errors)
validate_preview_tags(locale,
merged_tags(tag_sources, provenance['tags']),
validate_preview_tags(merged_tags(tag_sources, provenance['tags']),
validation_errors,
known_tags)
validate_parents(attributes['parent_post_ids'], validation_errors, existing_parent_ids)
@@ -348,7 +346,7 @@ class PostImportPreviewer
{ data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } }
end
def preload_known_tags locale, prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts
def preload_known_tags prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts
names = prepared_rows.flat_map { |row|
attributes = initial_attributes(row)
provenance = initial_provenance(row)
@@ -360,13 +358,15 @@ class PostImportPreviewer
metadata = metadata_for(row[:normal_url], metadata_cache)
apply_metadata!(attributes, provenance, tag_sources, metadata[:data])
end
preview_tag_names(locale, merged_tags(tag_sources, provenance['tags']))
preview_tag_names(merged_tags(tag_sources, provenance['tags']))
}.compact.uniq
return { } if names.empty?
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
.includes(:tag)
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
Tag.joins(:tag_name)
.where(tag_names: { name: names })
.includes(:tag_name)
.to_a
.index_by(&:name)
end
def preload_parent_ids prepared_rows
@@ -437,21 +437,19 @@ class PostImportPreviewer
sources['automatic'].to_s
end
def preview_tag_names locale, raw
def preview_tag_names raw
names = raw.to_s.split
return [] if names.empty?
if names.any? { _1.downcase.start_with?('nico:') }
return []
end
names.map { |name|
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
}
names.map { |name| TagName.canonicalise(name.sub(/\[.*\]\z/, '')).first }
rescue Tag::SectionLiteralParseError
[]
end
def validate_preview_tags locale, raw, errors, known_tags
def validate_preview_tags raw, errors, known_tags
names = raw.to_s.split
return if names.empty?
if names.any? { _1.downcase.start_with?('nico:') }
@@ -459,11 +457,9 @@ class PostImportPreviewer
return
end
parsed = names.map { |name|
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
}
parsed = names.map { |name| TagName.canonicalise(name.sub(/\[.*\]\z/, '')).first }
existing = parsed.filter_map { known_tags[_1] }
deprecated = existing.select(&:deprecated?).map { _1.name(locale.language_code) }
deprecated = existing.select(&:deprecated?).map(&:name)
errors[:tags] = ["廃止済みタグがあります: #{ deprecated.join(' ') }"] if deprecated.present?
rescue Tag::SectionLiteralParseError
errors[:tags] = ['タグ区間の記法が不正です.']
+3 -3
ファイルの表示
@@ -47,9 +47,9 @@ class PostMetadataFetcher
existing_tags =
Tag
.joins(:tag_names)
.where(tag_names: { language_code: 'ja', primary_flg: true, name: names })
.index_by { _1.name('ja') }
.joins(:tag_name)
.where(tag_names: { name: names })
.index_by(&:name)
names.map { |name|
tag = existing_tags[name]
+3 -18
ファイルの表示
@@ -1,5 +1,5 @@
class TagVersionRecorder < VersionRecorder
def self.record! tag:, event_type:, created_by_user:, **_options
def self.record! tag:, event_type:, created_by_user:
new(tag:, event_type:, created_by_user:).record!
end
@@ -14,25 +14,10 @@ class TagVersionRecorder < VersionRecorder
def record_key = :tag
def snapshot_attributes
names = snapshot_names
japanese_names = names.select { _1['language_code'] == 'ja' }
compatibility_names = japanese_names.presence || names
{ name: compatibility_names.find { _1['primary_flg'] }['name'],
{ name: @record.name,
category: @record.category,
deprecated_at: @record.deprecated_at,
aliases: compatibility_names.reject { _1['primary_flg'] }.map { _1['name'] }.join(' '),
names:,
aliases: @record.snapshot_aliases.join(' '),
parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') }
end
def snapshot_names
@record.tag_names.order(:language_code, primary_flg: :desc).order(:name).map { |tag_name|
{ 'language_code' => tag_name.language_code,
'name' => tag_name.name,
'script_code' => tag_name.script_code,
'primary_flg' => tag_name.primary_flg,
'auto_generated' => tag_name.auto_generated }
}
end
end
+8 -8
ファイルの表示
@@ -1,22 +1,22 @@
class TagVersioning
def self.record! tag, event_type:, created_by_user:, language_code: 'ja'
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
def self.record! tag, event_type:, created_by_user:
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
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?
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:, language_code:)
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:)
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
record!(tag, event_type:, created_by_user:, language_code:)
record!(tag, event_type:, created_by_user:)
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|
record_tag_snapshot!(tag, created_by_user:, language_code:)
record_tag_snapshot!(tag, created_by_user:)
end
end
end
+3 -3
ファイルの表示
@@ -41,7 +41,7 @@ class TheatrePostSelector
def weighted_candidates
@weighted_candidates ||= begin
penalties = tag_penalties
posts = eligible_posts.includes(tags: :tag_names).to_a
posts = eligible_posts.includes(tags: :tag_name).to_a
posts.map do |post|
post_tags = post.tags.to_a
@@ -82,7 +82,7 @@ class TheatrePostSelector
def tag_penalty_json
return [] if tag_penalties.empty?
tags = Tag.where(id: tag_penalties.keys).includes(:tag_names).index_by(&:id)
tags = Tag.where(id: tag_penalties.keys).includes(:tag_name).index_by(&:id)
tag_penalties
.map { |tag_id, penalty|
@@ -113,7 +113,7 @@ class TheatrePostSelector
def light_tag_json tag
{ id: tag.id,
name: tag.name('ja'),
name: tag.name,
category: tag.category }
end
end
-20
ファイルの表示
@@ -1,20 +0,0 @@
class CreateLanguages < ActiveRecord::Migration[8.0]
def up
create_table :languages, id: { type: :string, limit: 16 }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
end
execute <<~SQL
INSERT INTO
languages(code, name, created_at)
VALUES
('ja', '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :languages
end
end
-20
ファイルの表示
@@ -1,20 +0,0 @@
class CreateScripts < ActiveRecord::Migration[8.0]
def up
create_table :scripts, id: { type: 'CHAR(4)' }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
end
execute <<~SQL
INSERT INTO
scripts(code, name, created_at)
VALUES
('Jpan', '漢字および仮名文字', #{ connection.quote Time.current })
SQL
end
def down
drop_table :scripts
end
end
-25
ファイルの表示
@@ -1,25 +0,0 @@
class CreateLocales < ActiveRecord::Migration[8.0]
def up
create_table :locales, id: { type: :string, limit: 32 }, primary_key: :code do |t|
t.string :language_code, limit: 16, null: false, index: true
t.column :script_code, 'CHAR(4)', null: false
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
t.foreign_key :languages, column: :language_code, primary_key: :code
t.foreign_key :scripts, column: :script_code, primary_key: :code
end
execute <<~SQL
INSERT INTO
locales(code, language_code, script_code, name, created_at)
VALUES
('ja', 'ja', 'Jpan', '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :locales
end
end
-59
ファイルの表示
@@ -1,59 +0,0 @@
class AddColumnsToTagNames < ActiveRecord::Migration[8.0]
def up
add_reference :tag_names, :tag, after: :id, foreign_key: true
add_column :tag_names, :language_code, :string,
limit: 16, null: false, after: :tag_id, default: 'ja'
add_column :tag_names, :script_code, 'CHAR(4)',
null: false, after: :name, default: 'Jpan'
add_column :tag_names, :primary_flg, :boolean,
null: false, after: :script_code, default: true
add_foreign_key :tag_names, :languages, column: :language_code, primary_key: :code
add_foreign_key :tag_names, :scripts, column: :script_code, primary_key: :code
remove_index :tag_names, :name
add_index :tag_names, [:language_code, :name], unique: true
change_column_default :tag_names, :language_code, from: 'ja', to: nil
change_column_default :tag_names, :script_code, from: 'Jpan', to: nil
execute <<~SQL
UPDATE
tag_names tn
LEFT JOIN
tags AS t
ON
t.tag_name_id = COALESCE(tn.canonical_id, tn.id)
SET
tn.tag_id = t.id
, tn.primary_flg = CASE
WHEN tn.canonical_id IS NULL THEN
1
ELSE
0
END
SQL
change_column_default :tag_names, :primary_flg, from: true, to: nil
add_column :tag_names, :primary_tag_id, :bigint,
as: 'CASE WHEN primary_flg THEN tag_id ELSE NULL END'
add_index :tag_names, [:primary_tag_id, :language_code], unique: true
end
def down
remove_index :tag_names, [:primary_tag_id, :language_code]
remove_column :tag_names, :primary_tag_id
remove_foreign_key :tag_names, column: :language_code
remove_foreign_key :tag_names, column: :script_code
remove_index :tag_names, [:language_code, :name]
add_index :tag_names, :name, unique: true
remove_column :tag_names, :primary_flg
remove_column :tag_names, :script_code
remove_column :tag_names, :language_code
remove_reference :tag_names, :tag, foreign_key: true
end
end
-38
ファイルの表示
@@ -1,38 +0,0 @@
class AddNamesToTagVersions < ActiveRecord::Migration[8.0]
class TagVersion < ActiveRecord::Base
self.table_name = 'tag_versions'
end
def up
add_column :tag_versions, :names, :json
TagVersion.reset_column_information
say_with_time 'Backfilling multilingual tag version names' do
TagVersion.find_each do |version|
names = [{
language_code: 'ja',
name: version.name,
script_code: 'Jpan',
primary_flg: true,
auto_generated: false }]
names.concat(version.aliases.to_s.split.filter_map { |name|
next if name.blank?
{
language_code: 'ja',
name:,
script_code: 'Jpan',
primary_flg: false,
auto_generated: false }
})
version.update_columns(names: names)
end
end
change_column_null :tag_versions, :names, false
end
def down
remove_column :tag_versions, :names
end
end
-19
ファイルの表示
@@ -1,19 +0,0 @@
class AddAutoGeneratedToTagNames < ActiveRecord::Migration[8.0]
class TagVersion < ActiveRecord::Base
self.table_name = 'tag_versions'
end
def up
add_column :tag_names, :auto_generated, :boolean, null: false, default: false
TagVersion.reset_column_information
TagVersion.find_each do |version|
names = Array(version.names).map { |name| name.merge('auto_generated' => false) }
version.update_columns(names: names)
end
end
def down
remove_column :tag_names, :auto_generated
end
end
生成ファイル
+2 -42
ファイルの表示
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@@ -138,24 +138,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true
end
create_table "languages", primary_key: "code", id: { type: :string, limit: 16 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_languages_on_deprecated_at"
end
create_table "locales", primary_key: "code", id: { type: :string, limit: 32 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "language_code", limit: 16, null: false
t.string "script_code", limit: 4, null: false
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_locales_on_deprecated_at"
t.index ["language_code"], name: "index_locales_on_language_code"
t.index ["script_code"], name: "fk_rails_0b74ce96a8"
end
create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "material_id", null: false
t.string "profile", default: "legacy_drive", null: false
@@ -406,13 +388,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
end
create_table "scripts", primary_key: "code", id: { type: :string, limit: 4 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_scripts_on_deprecated_at"
end
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "user_id", null: false
t.datetime "created_at", null: false
@@ -445,21 +420,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
end
create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "tag_id"
t.string "language_code", limit: 16, null: false
t.string "name", null: false
t.string "script_code", limit: 4, null: false
t.boolean "primary_flg", null: false
t.bigint "canonical_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.virtual "primary_tag_id", type: :bigint, as: "(case when `primary_flg` then `tag_id` else NULL end)"
t.boolean "auto_generated", default: false, null: false
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
t.index ["language_code", "name"], name: "index_tag_names_on_language_code_and_name", unique: true
t.index ["primary_tag_id", "language_code"], name: "index_tag_names_on_primary_tag_id_and_language_code", unique: true
t.index ["script_code"], name: "fk_rails_dd783b3d1c"
t.index ["tag_id"], name: "index_tag_names_on_tag_id"
t.index ["name"], name: "index_tag_names_on_name", unique: true
end
create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -481,7 +447,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.bigint "created_by_user_id"
t.json "names", null: false
t.index ["created_at"], name: "index_tag_versions_on_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
@@ -736,8 +701,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
add_foreign_key "gekanator_question_suggestions", "users"
add_foreign_key "gekanator_questions", "gekanator_question_suggestions"
add_foreign_key "gekanator_questions", "users", column: "created_by_id"
add_foreign_key "locales", "languages", column: "language_code", primary_key: "code"
add_foreign_key "locales", "scripts", column: "script_code", primary_key: "code"
add_foreign_key "material_export_items", "materials"
add_foreign_key "material_export_items", "users", column: "created_by_user_id"
add_foreign_key "material_import_blocks", "users", column: "created_by_user_id"
@@ -771,10 +734,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do
add_foreign_key "settings", "users"
add_foreign_key "tag_implications", "tags"
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
add_foreign_key "tag_names", "languages", column: "language_code", primary_key: "code"
add_foreign_key "tag_names", "scripts", column: "script_code", primary_key: "code"
add_foreign_key "tag_names", "tag_names", column: "canonical_id"
add_foreign_key "tag_names", "tags"
add_foreign_key "tag_similarities", "tags"
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
add_foreign_key "tag_versions", "users", column: "created_by_user_id"
-1
ファイルの表示
@@ -11,7 +11,6 @@ RSpec.describe 'discarded tag cleanup migrations' do
end
context 'with legacy records' do
# Reproduce the historical schema required by these pre-multilingual migrations.
self.use_transactional_tests = false
before do
+1 -9
ファイルの表示
@@ -1,13 +1,5 @@
FactoryBot.define do
factory :tag_name do
language_code { 'ja' }
script_code { 'Jpan' }
primary_flg { true }
auto_generated { false }
sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" }
trait :alias do
primary_flg { false }
end
name { "tag-#{SecureRandom.hex(4)}" }
end
end
+3 -17
ファイルの表示
@@ -1,29 +1,15 @@
FactoryBot.define do
factory :tag do
transient do
primary_name { nil }
primary_tag_name do
attributes = primary_name.nil? ? { } : { name: primary_name }
build(:tag_name, **attributes)
end
name { nil }
end
category { :general }
post_count { 0 }
association :tag_name
after(:build) do |tag, evaluator|
evaluator.primary_tag_name.primary_flg = true
tag.tag_names.target << evaluator.primary_tag_name
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)
tag.name = evaluator.name if evaluator.name.present?
end
end
end
-71
ファイルの表示
@@ -1,71 +0,0 @@
require 'rails_helper'
RSpec.describe Locale, type: :model do
def prepare_french_reference!
Language.find_or_create_by!(code: 'fr') { _1.name = 'French' }
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
end
def create_french_locale!
prepare_french_reference!
described_class.create!(code: 'fr', language_code: 'fr',
script_code: 'Latn', name: 'French')
end
it 'generates a primary name for every existing tag using the name generator' do
first = create(:tag, primary_name: 'first_existing_tag')
second = create(:tag, primary_name: 'second_existing_tag')
allow(TagName).to receive(:generate_name).and_call_original
allow(TagName).to receive(:generate_name)
.with(kind_of(described_class), first, primary_tag_name_for(first, 'ja').name)
.and_return('name_from_generator')
locale = create_french_locale!
expect(TagName).to have_received(:generate_name)
.with(locale, first, primary_tag_name_for(first, 'ja').name)
expect(TagName).to have_received(:generate_name)
.with(locale, second, primary_tag_name_for(second, 'ja').name)
expect(first.tag_names.find_by!(language_code: 'fr', primary_flg: true))
.to have_attributes(name: 'name_from_generator', tag_id: first.id,
language_code: 'fr', script_code: 'Latn',
primary_flg: true)
expect(second.tag_names.find_by!(language_code: 'fr', primary_flg: true))
.to have_attributes(tag_id: second.id, language_code: 'fr',
script_code: 'Latn', primary_flg: true)
expect(TagName.where(language_code: 'fr', primary_flg: true,
tag_id: [first.id, second.id]).count).to eq(2)
end
it 'does not duplicate an existing primary name in the new language' do
prepare_french_reference!
tag = create(:tag, primary_name: 'already_named')
existing = create(:tag_name, name: 'nom_existant', tag:,
language_code: 'fr', script_code: 'Latn')
create_french_locale!
expect(TagName.where(tag_id: tag.id, language_code: 'fr', primary_flg: true))
.to contain_exactly(existing)
expect(existing.reload).to have_attributes(
tag_id: tag.id, language_code: 'fr', script_code: 'Latn', primary_flg: true)
end
it 'creates a primary name when the language has only an alias' do
prepare_french_reference!
tag = create(:tag, primary_name: 'alias_only_tag')
alias_name = create(:tag_name, :alias, name: 'alias_fr',
tag:,
language_code: 'fr', script_code: 'Latn')
create_french_locale!
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
language_code: 'fr', script_code: 'Latn', primary_flg: false)
generated = TagName.find_by!(tag_id: tag.id, language_code: 'fr', primary_flg: true)
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'fr',
script_code: 'Latn', primary_flg: true)
expect(TagName.where(tag_id: tag.id, language_code: 'fr', primary_flg: true).count).to eq(1)
end
end
+1 -1
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe MaterialExportItem, type: :model do
let(:user) { create(:user, :member) }
let(:tag) { create(:tag, primary_name: 'export_item', category: :material) }
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'export_item'), category: :material) }
let(:material) do
Material.create!(tag:, url: 'https://example.com/material',
created_by_user: user, updated_by_user: user)
+1 -1
ファイルの表示
@@ -18,7 +18,7 @@ RSpec.describe Post, type: :model do
expect(post.snapshot_tags_json).to eq([
{ 'tag_id' => tag.id,
'version_no' => tag.version_no,
'name' => primary_tag_name_for(tag, 'ja').name,
'name' => tag.name,
'category' => tag.category,
'sections' => [{ 'begin_ms' => 2000, 'end_ms' => nil }] },
{ 'external_tag_id' => external.id }])
+2 -2
ファイルの表示
@@ -1,8 +1,8 @@
require 'rails_helper'
RSpec.describe PostVersion, type: :model do
let!(:tag_name) { create(:tag_name, name: 'post_version_spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
let!(:tag_name) { TagName.create!(name: 'post_version_spec_tag') }
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
let!(:post_record) do
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
+5 -5
ファイルの表示
@@ -2,8 +2,8 @@ require 'rails_helper'
RSpec.describe TagImplication, type: :model do
it 'rejects a parent tag that would create a cycle' do
child = create(:tag, primary_name: 'tag_implication_cycle_child')
parent = create(:tag, primary_name: 'tag_implication_cycle_parent')
child = create(:tag, name: 'tag_implication_cycle_child')
parent = create(:tag, name: 'tag_implication_cycle_parent')
described_class.create!(tag: child, parent_tag: parent)
@@ -17,9 +17,9 @@ RSpec.describe TagImplication, type: :model do
end
it 'terminates even when existing data already contains a cycle' do
child = create(:tag, primary_name: 'tag_implication_existing_cycle_child')
parent = create(:tag, primary_name: 'tag_implication_existing_cycle_parent')
ancestor = create(:tag, primary_name: 'tag_implication_existing_cycle_ancestor')
child = create(:tag, name: 'tag_implication_existing_cycle_child')
parent = create(:tag, name: 'tag_implication_existing_cycle_parent')
ancestor = create(:tag, name: 'tag_implication_existing_cycle_ancestor')
described_class.create!(tag: parent, parent_tag: ancestor)
described_class.insert_all!(
+19 -59
ファイルの表示
@@ -34,41 +34,9 @@ RSpec.describe TagNameSanitisationRule, type: :model do
described_class.create!(priority: 10, source_pattern: '_', replacement: '')
end
context 'when only another language has the sanitised name' do
include_context 'English locale'
it 'keeps both names when sanitisation produces a cross-language match' do
japanese = create(:tag_name, name: 'foobar')
english = create(:tag_name, name: 'temporary',
language_code: 'en', script_code: 'Latn')
english.update_columns(name: 'foo_bar')
expect { described_class.apply! }.not_to change(TagName, :count)
expect(english.reload).to have_attributes(name: 'foobar', language_code: 'en')
expect(japanese.reload).to have_attributes(name: 'foobar', language_code: 'ja')
end
it 'renames without merging tags or deleting either language identity' do
japanese = create(:tag, primary_name: 'foobar')
english_name = create(:tag_name, name: 'temporary',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
english_name.update_columns(name: 'foo_bar')
described_class.apply!
expect(english_name.reload).to have_attributes(
name: 'foobar', language_code: 'en', tag_id: english.id)
expect(primary_tag_name_for(japanese.reload, 'ja')).to have_attributes(
name: 'foobar', language_code: 'ja', tag_id: japanese.id)
expect(primary_tag_name_for(english.reload, 'en')).to eq(english_name)
end
end
context 'when no conflicting tag_name exists' do
let!(:tag_name) do
create(:tag_name, name: 'tmp').tap do |tn|
TagName.create!(name: 'tmp').tap do |tn|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end
end
@@ -79,10 +47,10 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end
end
context 'when a conflicting primary tag name exists' do
let!(:existing) { create(:tag_name, name: 'foobar') }
context 'when a conflicting canonical tag_name exists' do
let!(:existing) { TagName.create!(name: 'foobar') }
let!(:source) do
create(:tag_name, name: 'tmp').tap do |tn|
TagName.create!(name: 'tmp').tap do |tn|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end
end
@@ -95,56 +63,48 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end
context 'when the source tag_name has a tag and the existing one has no tag' do
let!(:existing) { create(:tag_name, name: 'foobar') }
let!(:source_tag) { create(:tag, primary_name: 'tmp', category: :general) }
let!(:source_tag_name_id) { primary_tag_name_for(source_tag, 'ja').id }
let!(:existing) { TagName.create!(name: 'foobar') }
let!(:source_tag) { create(:tag, name: 'tmp', category: :general) }
let!(:source_tag_name_id) { source_tag.tag_name_id }
before do
primary_tag_name_for(source_tag, 'ja').update_columns(
name: 'foo_bar', updated_at: Time.current)
source_tag.tag_name.update_columns(name: 'foo_bar', updated_at: Time.current)
end
it 'moves the tag to the existing tag_name' do
described_class.apply!
expect(existing.reload).to have_attributes(
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
expect(primary_tag_name_for(source_tag, 'ja')).to eq(existing)
expect(primary_tag_name_for(source_tag.reload, 'ja')).to eq(existing)
expected_tag_name_id = existing.canonical_id || existing.id
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
end
end
context 'when the sanitised name is an alias of an existing tag' do
let!(:existing_tag) { create(:tag) }
let!(:existing_primary) { primary_tag_name_for(existing_tag, 'ja') }
let!(:alias_name) do
create(:tag_name, :alias, name: 'foobar', tag: existing_tag)
TagName.create!(name: 'foobar', canonical: existing_tag.tag_name)
end
let!(:source) do
create(:tag_name, name: 'tmp').tap do |tn|
TagName.create!(name: 'tmp').tap do |tn|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end
end
it 'deletes 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!
expect(TagName.unscoped.exists?(source.id)).to be(false)
expect(alias_name.reload).to have_attributes(
tag_id: existing_tag.id, language_code: 'ja', primary_flg: false)
expect(TagName.find_by!(tag_id: existing_tag.id,
language_code: 'ja', primary_flg: true))
.to eq(existing_primary)
expect(alias_name.reload.canonical).to eq(existing_tag.tag_name)
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
end
end
context 'when both source and existing tag_names have tags' do
let!(:existing_tn) { create(:tag_name, name: 'foobar') }
let!(:existing_tag) { create(:tag, primary_tag_name: existing_tn, category: :general) }
let!(:existing_tn) { TagName.create!(name: 'foobar') }
let!(:existing_tag) { Tag.create!(tag_name: existing_tn, category: :general) }
let!(:source_tn) { create(:tag_name, name: 'tmp') }
let!(:source_tag) { create(:tag, primary_tag_name: source_tn, category: :general) }
let!(:source_tn) { TagName.create!(name: 'tmp') }
let!(:source_tag) { Tag.create!(tag_name: source_tn, category: :general) }
let!(:source_tag_name_id) { source_tn.id }
before do
@@ -160,7 +120,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
expect(post.reload.tags).to contain_exactly(existing_tag)
expect(primary_tag_name_for(existing_tag.reload, 'ja').name).to eq('foobar')
expect(existing_tag.reload.name).to eq('foobar')
end
end
end
-127
ファイルの表示
@@ -1,127 +0,0 @@
require 'rails_helper'
RSpec.describe TagName, type: :model do
include_context 'English locale'
it 'allows the same name in different languages' do
create(:tag_name, name: 'shared_name')
english = build(:tag_name, name: 'shared_name',
language_code: 'en', script_code: 'Latn')
expect(english).to be_valid
expect { english.save! }.to change(described_class, :count).by(1)
end
it 'rejects a duplicate name within the same language' do
create(:tag_name, name: 'shared_name')
duplicate = build(:tag_name, name: 'shared_name')
expect(duplicate).to be_invalid
expect(duplicate.errors.of_kind?(:name, :taken)).to be(true)
end
it 'creates Japanese factory names without requiring a tag' do
name = create(:tag_name)
expect(name.reload).to have_attributes(
language_code: 'ja', script_code: 'Jpan', primary_flg: true, tag_id: nil)
end
it 'persists both sides of the representative tag factory association' do
name = create(:tag_name)
tag = create(:tag, primary_tag_name: name)
expect(name.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', script_code: 'Jpan', primary_flg: true)
expect(name.tag).to eq(tag)
expect(described_class.find_by!(tag_id: tag.id, language_code: 'ja', primary_flg: true))
.to eq(name)
expect(tag.reload.tag_name_id).to eq(name.id)
end
it 'builds a primary name without persisting the tag or its name' do
tag = nil
expect {
tag = build(:tag, primary_name: 'built_primary')
}.to change(Tag, :count).by(0).and change(described_class, :count).by(0)
expect(tag).to be_new_record
expect(tag.tag_names.target).to contain_exactly(have_attributes(
name: 'built_primary', language_code: 'ja', script_code: 'Jpan',
primary_flg: true))
end
it 'persists an alias owned by the same tag as its primary name' do
primary_name = create(:tag_name)
tag = create(:tag, primary_tag_name: primary_name)
alias_name = create(:tag_name, :alias, name: 'valid_alias', tag:)
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
primary_flg: false, language_code: 'ja')
primary = described_class.find_by!(tag_id: alias_name.tag_id,
language_code: alias_name.language_code,
primary_flg: true)
expect(primary).to eq(primary_name)
end
describe '.canonicalise' do
it 'leaves detached aliases unchanged' do
create(:tag_name, :alias, name: 'detached_alias')
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
.to eq(['detached_alias'])
end
it 'leaves an alias unchanged when its language has no primary' do
tag = create(:tag, primary_name: '日本語のみ')
create(:tag_name, :alias, tag:, name: 'orphan_english_alias',
language_code: 'en', script_code: 'Latn')
expect(described_class.canonicalise(locale, ['orphan_english_alias']))
.to eq(['orphan_english_alias'])
end
it 'resolves the primary name of the alias language on the same tag' do
tag = create(:tag, primary_name: '日本語正本')
english_primary = create(:tag_name, tag:, name: 'english_primary',
language_code: 'en', script_code: 'Latn')
english_alias = create(:tag_name, :alias, tag:, name: 'english_alias',
language_code: 'en', script_code: 'Latn')
representative_id = tag.tag_name_id
primary = described_class.find_by!(tag_id: english_alias.tag_id,
language_code: english_alias.language_code,
primary_flg: true)
expect(primary).to eq(english_primary)
expect(described_class.canonicalise(locale, [english_alias.name]))
.to eq([english_primary.name])
expect(tag.reload.tag_name_id).to eq(representative_id)
end
it 'resolves only aliases in the requested language' do
japanese = create(:tag, primary_name: 'japanese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: japanese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
expect(japanese_alias.reload).to have_attributes(
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(described_class.canonicalise(locale, ['shared_alias']))
.to eq(['english_canonical'])
expect(described_class.canonicalise(Locale.nipponese, ['shared_alias']))
.to eq(['japanese_canonical'])
end
end
end
+86 -232
ファイルの表示
@@ -4,12 +4,12 @@ RSpec.describe Tag, type: :model do
describe 'external tag separation' do
['nico:reserved', 'NiCo:reserved'].each do |name|
it "rejects the reserved prefix #{ name } for internal tags" do
tag = build(:tag, primary_name: name)
tag = build(:tag, name:)
expect(tag).to be_invalid
expect(tag.errors[:name]).to be_present
expect {
described_class.normalise_tags!(Locale.nipponese, [name])
described_class.normalise_tags!([name])
}.to raise_error(Tag::NicoTagNormalisationError)
end
end
@@ -25,160 +25,128 @@ RSpec.describe Tag, type: :model do
end
describe '.normalise_tags!' do
it 'canonicalises aliases in the supplied language only' do
Language.find_or_create_by!(code: 'en') { _1.name = 'English' }
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
Locale.insert_all!([
{ code: 'en', language_code: 'en', script_code: 'Latn',
name: 'English', created_at: Time.current }]) unless Locale.exists?(code: 'en')
english_locale = Locale.find('en')
japanese = create(:tag, primary_name: 'japanese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: japanese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
expect(japanese_alias.reload).to have_attributes(
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(described_class.normalise_tags!(
english_locale, ['shared_alias'],
with_tagme: false, with_no_deerjikist: false)).to eq([english])
expect(described_class.normalise_tags!(
Locale.nipponese, ['shared_alias'],
with_tagme: false, with_no_deerjikist: false)).to eq([japanese])
end
it 'rejects deprecated tags when deny_deprecated is enabled' do
tag_name = create(:tag_name, name: 'normalise deprecated tag')
deprecated_tag = create(:tag,
primary_tag_name: tag_name,
tag_name = TagName.create!(name: 'normalise deprecated tag')
deprecated_tag = Tag.create!(
tag_name:,
category: :general,
deprecated_at: 1.day.from_now)
deprecated_at: 1.day.from_now
)
expect {
described_class.normalise_tags!(Locale.nipponese,
[primary_tag_name_for(deprecated_tag, 'ja').name],
deny_deprecated: true)
described_class.normalise_tags!(
[deprecated_tag.name],
deny_deprecated: true
)
}.to raise_error(Tag::DeprecatedTagNormalisationError) { |error|
expect(error.tag_names).to eq([primary_tag_name_for(deprecated_tag, 'ja').name])
expect(error.tag_names).to eq([deprecated_tag.name])
}
end
it 'rejects invalid section literals instead of treating them as zero' do
expect {
described_class.normalise_tags!(Locale.nipponese,
described_class.normalise_tags!(
['normalise_invalid_section[1:aa-2:00]'],
with_sections: true)
with_sections: true
)
}.to raise_error(Tag::SectionLiteralParseError)
end
it 'parses open-ended section literals' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
it 'parses omitted begin as zero' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[-1:00]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
end
it 'treats fully open section literals as plain tags' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[-]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections)[tag.id]).to be_nil
end
it 'treats [0:00-] as a plain tag' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[0:00-]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections)[tag.id]).to be_nil
end
it 'expands zero-width sections to one millisecond' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-1:00]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
end
it 'swaps reversed section boundaries' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[2:00-1:00]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
end
it 'merges open-ended sections over later bounded sections' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-][2:00-3:00]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
it 'merges adjacent bounded and open-ended sections' do
result = described_class.normalise_tags!(Locale.nipponese,
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-3:00][3:00-]'],
with_sections: true)
with_sections: true
)
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
end
describe '.expand_parent_tags' do
it 'expands through multiple deprecated parents to an active ancestor' do
child = create(:tag, primary_name: 'expand_child')
deprecated_parent = create(:tag,
primary_name: 'expand_deprecated_parent',
deprecated_at: Time.current)
deprecated_grandparent = create(:tag,
primary_name: 'expand_deprecated_grandparent',
deprecated_at: Time.current)
active_ancestor = create(:tag, primary_name: 'expand_active_ancestor')
child = create(:tag, name: 'expand_child')
deprecated_parent = create(
:tag,
name: 'expand_deprecated_parent',
deprecated_at: Time.current
)
deprecated_grandparent = create(
:tag,
name: 'expand_deprecated_grandparent',
deprecated_at: Time.current
)
active_ancestor = create(:tag, name: 'expand_active_ancestor')
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
@@ -195,8 +163,8 @@ RSpec.describe Tag, type: :model do
end
it 'terminates when implications contain a cycle' do
first = create(:tag, primary_name: 'expand_cycle_first')
second = create(:tag, primary_name: 'expand_cycle_second')
first = create(:tag, name: 'expand_cycle_first')
second = create(:tag, name: 'expand_cycle_second')
TagImplication.create!(tag: first, parent_tag: second)
now = Time.current
TagImplication.insert_all!(
@@ -215,156 +183,55 @@ RSpec.describe Tag, type: :model do
end
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
tag = described_class.find_or_create_by_tag_name!(
locale, 'english_name', category: :character)
expect(primary_tag_name_for(tag, 'en')).to have_attributes(
language_code: 'en', script_code: 'Latn',
primary_flg: true, tag_id: tag.id)
expect(tag.category).to eq('character')
end
it 'creates separate tag identities for the same name in different languages' do
japanese = described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'same_name', category: :general)
english = described_class.find_or_create_by_tag_name!(
locale, 'same_name', category: :character)
expect(english).not_to eq(japanese)
expect(primary_tag_name_for(english, 'en')).to have_attributes(
name: 'same_name', tag_id: english.id, language_code: 'en', primary_flg: true)
expect(primary_tag_name_for(japanese, 'ja')).to have_attributes(
name: 'same_name', tag_id: japanese.id, language_code: 'ja', primary_flg: true)
expect(described_class.find_or_create_by_tag_name!(
locale, 'same_name', category: :general)).to eq(english)
expect(described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'same_name', category: :general)).to eq(japanese)
end
it 'preserves the V1 representative when looking up another primary language' do
tag = create(:tag, primary_name: '日本語代表名')
representative_id = tag.tag_name_id
english = create(:tag_name, name: 'english_primary', tag:,
language_code: 'en', script_code: 'Latn')
found = described_class.find_or_create_by_tag_name!(
locale, english.name, category: :general)
expect(found).to eq(tag)
expect(tag.reload.tag_name_id).to eq(representative_id)
expect(english.reload.tag_id).to eq(tag.id)
end
it 'normalises names using the supplied locale' do
tags = described_class.normalise_tags!(
locale, ['character:normalised_english'],
with_tagme: false, with_no_deerjikist: false)
expect(tags.length).to eq(1)
expect(primary_tag_name_for(tags.first, 'en')).to have_attributes(
name: 'normalised_english', language_code: 'en', script_code: 'Latn',
primary_flg: true, tag_id: tags.first.id)
end
end
it 'creates a tag and name with the requested category after stripping whitespace' do
tag = nil
expect {
tag = described_class.find_or_create_by_tag_name!(Locale.nipponese,
tag = described_class.find_or_create_by_tag_name!(
' lookup_new ', category: :character)
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
expect(primary_tag_name_for(tag, 'ja').name).to eq('lookup_new')
expect(tag.name).to eq('lookup_new')
expect(tag.category).to eq('character')
end
it 'reuses the owning tag for an alias without changing its category' do
it 'reuses the canonical tag for an alias without changing its category' do
tag = create(:tag, category: :character)
representative_id = tag.tag_name_id
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
primary_flg: false, language_code: 'ja')
alias_name = TagName.create!(name: 'lookup_alias', canonical: tag.tag_name)
found = nil
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)
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
expect(found).to eq(tag)
expect(found.category).to eq('character')
expect(tag.reload.tag_name_id).to eq(representative_id)
end
it 'reuses the owning tag through another alias' do
tag = create(:tag)
primary = primary_tag_name_for(tag, 'ja')
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
other_alias = create(:tag_name, :alias, name: 'lookup_other_alias', tag:)
expect([alias_name.reload, other_alias.reload]).to all(have_attributes(
tag_id: tag.id,
primary_flg: false, language_code: 'ja'))
it 'creates a tag for an existing canonical name reached through an alias' do
canonical = create(:tag_name)
alias_name = TagName.create!(name: 'lookup_alias', canonical:)
tag = nil
expect {
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)
expect(found).to eq(tag)
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
}.to change(Tag, :count).by(1).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
describe '.merge_tags!' do
let!(:target_tag) { create(:tag, category: :general) }
let!(:source_tag) { create(:tag, category: :general) }
let!(:source_tag_name) { primary_tag_name_for(source_tag, 'ja') }
let!(:source_tag_name) { source_tag.tag_name }
let!(:post_record) do
Post.create!(url: 'https://example.com/posts/1', title: 'test post')
end
context 'with names in multiple languages' do
include_context 'English locale'
it 'keeps the target primary and moves source names as aliases' do
english_primary = create(:tag_name, tag: target_tag,
name: 'target_english', language_code: 'en', script_code: 'Latn')
source_primary = create(:tag_name, tag: source_tag,
name: 'source_english', language_code: 'en', script_code: 'Latn')
source_alias = create(:tag_name, :alias, tag: source_tag,
name: 'source_english_alias', language_code: 'en', script_code: 'Latn')
described_class.merge_tags!(target_tag, [source_tag])
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(english_primary)
expect([source_primary.reload, source_alias.reload]).to all(have_attributes(
tag_id: target_tag.id, language_code: 'en', primary_flg: false))
end
it 'inherits a source primary when the target has no name in that language' do
source_primary = create(:tag_name, tag: source_tag,
name: 'inherited_english', language_code: 'en', script_code: 'Latn')
source_alias = create(:tag_name, :alias, tag: source_tag,
name: 'inherited_alias', language_code: 'en', script_code: 'Latn')
described_class.merge_tags!(target_tag, [source_tag])
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(source_primary)
expect(source_primary.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'en', primary_flg: true)
expect(source_alias.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'en', primary_flg: false)
end
end
context 'when merging a simple source tag' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
@@ -376,8 +243,7 @@ RSpec.describe Tag, type: :model do
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -403,16 +269,15 @@ RSpec.describe Tag, type: :model do
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(target_tag.reload.post_count).to eq(1)
end
end
it 'keeps source history and records the new target alias after deleting the source' do
user = create_member_user!
source_name = primary_tag_name_for(source_tag, 'ja').name
source_alias = create(:tag_name, :alias, name: 'merge_alias', tag: source_tag)
source_name = source_tag.name
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name)
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
original_version = source_tag.tag_versions.first
@@ -422,20 +287,13 @@ RSpec.describe Tag, type: :model do
expect(versions.pluck(:version_no, :event_type))
.to eq([[1, 'create'], [2, 'discard']])
expect(versions.first).to eq(original_version)
expect(versions.last.created_by_user).to eq(user)
expect(versions.last.names).to include(
hash_including('language_code' => 'ja', 'name' => source_name,
'primary_flg' => true, 'auto_generated' => false),
hash_including('language_code' => 'ja', 'name' => source_alias.name,
'primary_flg' => false, 'auto_generated' => false))
expect(versions.last).to have_attributes(
name: source_name, aliases: source_alias.name, created_by_user: user)
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
target_versions = target_tag.tag_versions.order(:version_no)
expect(target_versions.pluck(:event_type)).to eq(['create', 'update'])
target_aliases = target_versions.last.names.filter_map { |name|
name['name'] if name['language_code'] == 'ja' && !name['primary_flg']
}
expect(target_aliases).to contain_exactly('merge_alias', source_name)
expect(target_versions.last.aliases.split).to eq([source_name])
end
it 'deletes source relationships while preserving unrelated relationships' do
@@ -489,8 +347,7 @@ RSpec.describe Tag, type: :model do
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -521,8 +378,7 @@ RSpec.describe Tag, type: :model do
expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -541,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
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,
tag: source_tag,
begin_ms: 1000, end_ms: 2000)
@@ -551,8 +407,7 @@ RSpec.describe Tag, type: :model do
}.to raise_error(ActiveRecord::RecordInvalid)
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
expect(earlier_name.reload).to have_attributes(
tag_id: earlier_source.id, language_code: 'ja', primary_flg: true)
expect(earlier_name.reload.canonical_id).to be_nil
expect(TagVersion.where(tag_id: [earlier_source.id, source_tag.id, target_tag.id]))
.to be_empty
expect(Tag.unscoped.exists?(source_tag.id)).to be(true)
@@ -561,8 +416,7 @@ RSpec.describe Tag, type: :model do
expect(source_post_tag.sections).to contain_exactly(source_section)
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
expect(source_tag.reload.post_count).to eq(1)
expect(source_tag_name.reload).to have_attributes(
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
expect(source_tag_name.reload.canonical_id).to be_nil
expect(target_tag.reload.post_count).to eq(0)
end
end
+3 -7
ファイルの表示
@@ -1,7 +1,7 @@
require 'rails_helper'
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') }
it 'makes TagVersion read only after create' do
@@ -9,11 +9,9 @@ RSpec.describe VersionRecord, type: :model do
tag: tag,
version_no: 1,
event_type: 'create',
name: primary_tag_name_for(tag, 'ja').name,
name: tag.name,
category: tag.category,
aliases: '',
names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name,
script_code: 'Jpan', primary_flg: true }],
parent_tag_ids: '',
created_at: Time.current,
created_by_user: nil
@@ -29,11 +27,9 @@ RSpec.describe VersionRecord, type: :model do
tag: tag,
version_no: 1,
event_type: 'create',
name: primary_tag_name_for(tag, 'ja').name,
name: tag.name,
category: tag.category,
aliases: '',
names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name,
script_code: 'Jpan', primary_flg: true }],
parent_tag_ids: '',
created_at: Time.current,
created_by_user: nil
+1 -1
ファイルの表示
@@ -30,7 +30,7 @@ RSpec.describe 'Deerjikists API', type: :request do
expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag1.id)
expect(json['tag']['name']).to eq(primary_tag_name_for(tag1, 'ja').name)
expect(json['tag']['name']).to eq(tag1.name)
end
end
+1 -1
ファイルの表示
@@ -15,7 +15,7 @@ RSpec.describe 'error responses', type: :request do
it 'returns a stable field-error payload for unprocessable requests' do
member = create(:user, :member)
tag = create(:tag, category: :general, primary_name: 'error_response_tag')
tag = create(:tag, :general, name: 'error_response_tag')
sign_in_as(member)
patch "/tags/#{ tag.id }", params: { category: 'nico' }
+7 -6
ファイルの表示
@@ -898,22 +898,23 @@ RSpec.describe 'Gekanator learning API', type: :request do
describe 'GET /gekanator/questions' do
it 'omits questions for deprecated tags' do
active_tag = create(:tag, primary_name: 'active_question_tag', category: :general)
deprecated_tag = create(:tag,
primary_name: 'deprecated_question_tag',
active_tag = Tag.create!(name: 'active_question_tag', category: :general)
deprecated_tag = Tag.create!(
name: 'deprecated_question_tag',
category: :general,
deprecated_at: Time.current)
deprecated_at: Time.current
)
[active_tag, deprecated_tag].each do |question_tag|
GekanatorQuestion.create!(
text: "#{ primary_tag_name_for(question_tag, 'ja').name }?",
text: "#{ question_tag.name }?",
kind: 'tag',
source: 'admin_curated',
status: 'accepted',
priority_weight: 1.0,
condition: {
type: 'tag',
key: "#{ question_tag.category }:#{ primary_tag_name_for(question_tag, 'ja').name }"
key: "#{ question_tag.category }:#{ question_tag.name }"
},
created_by: admin
)
+5 -4
ファイルの表示
@@ -4,11 +4,12 @@ require 'rails_helper'
RSpec.describe 'Gekanator posts API', type: :request do
describe 'GET /gekanator/posts' do
it 'omits deprecated tags and returns the stored similarity cosine' do
active_tag = create(:tag, primary_name: 'active tag', category: :general)
deprecated_tag = create(:tag,
primary_name: 'deprecated tag',
active_tag = Tag.create!(name: 'active tag', category: :general)
deprecated_tag = Tag.create!(
name: 'deprecated tag',
category: :general,
deprecated_at: Time.current)
deprecated_at: Time.current
)
post_record = Post.create!(title: 'source', url: 'https://example.com/source')
target_post = Post.create!(title: 'target', url: 'https://example.com/target')
-237
ファイルの表示
@@ -1,237 +0,0 @@
require 'rails_helper'
RSpec.describe 'Locale propagation on write paths', type: :request do
include_context 'English locale'
let(:member) { create(:user, :member) }
before do
sign_in_as(member)
end
def expect_localised_tag(name, expected_locale)
tag_name = TagName.find_by!(name:, language_code: expected_locale.language_code)
expect(tag_name).to have_attributes(
script_code: expected_locale.script_code, primary_flg: true)
expect(tag_name.tag_id).to be_present
tag_name.tag
end
['en', nil].each do |requested_locale|
context "with locale #{ requested_locale.inspect }" do
let(:locale_params) { requested_locale ? { locale: requested_locale } : { } }
let(:expected_locale) { requested_locale ? locale : Locale.nipponese }
it 'creates post tags in the requested language or Japanese fallback' do
post '/posts', params: locale_params.merge(
title: 'Locale post', url: 'https://example.com/locale-post',
tags: 'locale_post_tag', parent_post_ids: '')
expect(response).to have_http_status(:created), response.body
tag = expect_localised_tag('locale_post_tag', expected_locale)
expect(Post.find(json.fetch('id')).tags).to include(tag)
end
it 'updates post tags in the requested language or Japanese fallback' do
record = create(:post)
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
put "/posts/#{ record.id }", params: locale_params.merge(
title: record.title, tags: 'locale_updated_tag', parent_post_ids: '',
base_version_no: record.reload.version_no)
expect(response).to have_http_status(:ok), response.body
tag = expect_localised_tag('locale_updated_tag', expected_locale)
expect(record.reload.tags).to include(tag)
end
it 'passes the resolved locale to bulk creation' do
# Worker propagation is exercised in post_bulk_creator_spec.
expect_any_instance_of(PostBulkCreator).to receive(:run)
.with(expected_locale).and_return(results: [])
manifest = [{ title: 'Bulk locale', url: 'https://example.com/bulk-locale',
tags: 'bulk_locale_tag', parent_post_ids: '' }]
post '/posts/bulk', params: locale_params.merge(posts: JSON.generate(manifest)),
headers: { 'CONTENT_TYPE' => 'multipart/form-data' }
expect(response).to have_http_status(:ok), response.body
end
it 'creates materials with tags in the resolved locale' do
post '/materials', params: locale_params.merge(
tag: 'locale_material', url: 'https://example.com/material')
expect(response).to have_http_status(:created), response.body
tag = expect_localised_tag('locale_material', expected_locale)
expect(Material.find(json.fetch('id')).tag).to eq(tag)
end
it 'updates materials with tags in the resolved locale' do
tag = create(:tag, category: :material)
material = Material.create!(tag:, url: 'https://example.com/material')
put "/materials/#{ material.id }", params: locale_params.merge(
tag: 'locale_material_updated', url: material.url)
expect(response).to have_http_status(:ok), response.body
resolved_tag = expect_localised_tag('locale_material_updated', expected_locale)
expect(material.reload.tag).to eq(resolved_tag)
end
it 'links Nico external tags to internal tags in the resolved locale' do
external = create(:external_tag)
NicoTagVersionRecorder.record!(
external_tag: external, event_type: :create, created_by_user: member)
put "/tags/nico/#{ external.id }", params: locale_params.merge(tags: 'locale_link')
expect(response).to have_http_status(:ok), response.body
tag = expect_localised_tag('locale_link', expected_locale)
expect(external.reload.linked_tags).to contain_exactly(tag)
end
it 'creates parent tags in the resolved locale' do
tag = create(:tag)
put "/tags/#{ tag.id }", params: locale_params.merge(
name: primary_tag_name_for(tag, 'ja').name, category: tag.category, deprecated: false,
aliases: '', parent_tags: 'locale_parent')
expect(response).to have_http_status(:ok), response.body
parent = expect_localised_tag('locale_parent', expected_locale)
expect(TagImplication.where(tag:).pluck(:parent_tag_id)).to eq([parent.id])
end
end
end
context 'with existing names in both languages' do
let!(:english_name) do
create(:tag_name, name: 'shared_name', language_code: 'en', script_code: 'Latn')
end
let!(:english_tag) { create(:tag, primary_tag_name: english_name, category: :material) }
let!(:japanese_tag) do
create(:tag, primary_name: 'temporary_japanese', category: :general).tap do |tag|
# Isolate lookup from the separately tested uniqueness validation.
primary_tag_name_for(tag, 'ja').update_columns(name: 'shared_name')
end
end
it 'plans and creates the post using the English identity and category' do
post '/posts', params: {
locale: 'en', title: 'English identity',
url: 'https://example.com/english-identity', tags: 'shared_name', parent_post_ids: '' }
expect(response).to have_http_status(:created), response.body
record = Post.find(json.fetch('id'))
expect(record.tags).to include(english_tag)
expect(record.tags).not_to include(japanese_tag)
expect(english_tag.reload.category).to eq('material')
expect(japanese_tag.reload.category).to eq('general')
end
it 'updates the post using the English identity' do
record = create(:post)
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
put "/posts/#{ record.id }", params: {
locale: 'en', title: record.title, tags: 'shared_name', parent_post_ids: '',
base_version_no: record.reload.version_no }
expect(response).to have_http_status(:ok), response.body
expect(record.reload.tags).to include(english_tag)
expect(record.tags).not_to include(japanese_tag)
end
it 'resolves the English material tag' do
post '/materials', params: {
locale: 'en', tag: 'shared_name', url: 'https://example.com/english-material' }
expect(response).to have_http_status(:created), response.body
expect(Material.find(json.fetch('id')).tag).to eq(english_tag)
expect(Material.where(tag: japanese_tag)).to be_empty
end
it 'resolves the English parent tag' do
child = create(:tag)
put "/tags/#{ child.id }", params: {
locale: 'en', name: primary_tag_name_for(child, 'ja').name,
category: child.category, deprecated: false,
aliases: '', parent_tags: 'shared_name' }
expect(response).to have_http_status(:ok), response.body
expect(TagImplication.where(tag: child).pluck(:parent_tag_id)).to eq([english_tag.id])
end
it 'resolves the English internal tag for a Nico link' do
external = create(:external_tag)
NicoTagVersionRecorder.record!(
external_tag: external, event_type: :create, created_by_user: member)
put "/tags/nico/#{ external.id }", params: { locale: 'en', tags: 'shared_name' }
expect(response).to have_http_status(:ok), response.body
expect(external.reload.linked_tags).to contain_exactly(english_tag)
end
end
context 'when renaming or updating aliases in English' do
let!(:english_name) do
create(:tag_name, name: 'english_original', language_code: 'en', script_code: 'Latn')
end
let!(:tag) { create(:tag, primary_tag_name: english_name) }
it 'allows a rename to a name already used in Japanese' do
japanese = create(:tag, primary_name: 'rename_target')
patch "/tags/#{ tag.id }", params: { locale: 'en', name: 'rename_target' }
expect(response).to have_http_status(:ok), response.body
expect(primary_tag_name_for(tag.reload, 'en')).to have_attributes(
name: 'rename_target', language_code: 'en', tag_id: tag.id)
expect(primary_tag_name_for(japanese.reload, 'ja')).to have_attributes(
name: 'rename_target', language_code: 'ja', tag_id: japanese.id)
end
it 'creates aliases in the requested language' do
put "/tags/#{ tag.id }", params: {
locale: 'en', name: english_name.name,
category: tag.category, deprecated: false,
aliases: 'english_alias', parent_tags: '' }
expect(response).to have_http_status(:ok), response.body
expect(TagName.find_by!(language_code: 'en', name: 'english_alias'))
.to have_attributes(
script_code: 'Latn',
tag_id: tag.id, primary_flg: false, language_code: 'en')
primary = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
expect(primary).to have_attributes(name: 'english_original', script_code: 'Latn')
end
it 'does not take an alias from another language' do
japanese = create(:tag, primary_name: 'japanese_owner')
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: japanese)
expect(japanese_alias.reload).to have_attributes(
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
put "/tags/#{ tag.id }", params: {
locale: 'en', name: english_name.name,
category: tag.category, deprecated: false,
aliases: 'shared_alias', parent_tags: '' }
expect(response).to have_http_status(:ok), response.body
expect(japanese_alias.reload).to have_attributes(
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(TagName.find_by!(language_code: 'en', name: 'shared_alias'))
.to have_attributes(
tag_id: tag.id, script_code: 'Latn',
primary_flg: false, language_code: 'en')
expect(TagName.where(name: 'shared_alias').pluck(:language_code, :tag_id))
.to contain_exactly(['ja', japanese.id], ['en', tag.id])
end
end
end
+23 -25
ファイルの表示
@@ -26,10 +26,10 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials' do
let!(:tag_a) do
create(:tag, primary_name: 'material_index_a', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'material_index_a'), category: :material)
end
let!(:tag_b) do
create(:tag, primary_name: 'material_index_b', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'material_index_b'), category: :material)
end
let!(:material_a) do
@@ -116,20 +116,20 @@ RSpec.describe 'Materials API', type: :request do
it 'filters by descendant tags and returns stable parent tag groups' do
root =
create(:tag, primary_name: 'material_scope_root',
Tag.create!(tag_name: TagName.create!(name: 'material_scope_root'),
category: :material)
child_b =
create(:tag, primary_name: 'material_scope_b',
Tag.create!(tag_name: TagName.create!(name: 'material_scope_b'),
category: :material)
child_a =
create(:tag, primary_name: 'material_scope_a',
Tag.create!(tag_name: TagName.create!(name: 'material_scope_a'),
category: :material)
deprecated =
create(:tag, primary_name: 'material_scope_old',
Tag.create!(tag_name: TagName.create!(name: 'material_scope_old'),
category: :material,
deprecated_at: Time.current)
grandchild =
create(:tag, primary_name: 'material_scope_grandchild',
Tag.create!(tag_name: TagName.create!(name: 'material_scope_grandchild'),
category: :material)
root_material =
build_material(tag: root, user: member_user,
@@ -179,7 +179,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials/:id' do
let!(:tag) do
create(:tag, primary_name: 'material_show', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
@@ -269,7 +269,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:created)
material = Material.order(:id).last
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_new')
expect(material.tag.name).to eq('material_create_new')
expect(material.tag.category).to eq('material')
expect(material.created_by_user).to eq(member_user)
expect(material.updated_by_user).to eq(member_user)
@@ -297,8 +297,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:created)
tag = TagName.find_by!(name: 'material_create_versioned_tag',
language_code: 'ja', primary_flg: true).tag
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_create_versioned_tag' })
version = tag.tag_versions.order(:version_no).last
expect(version.event_type).to eq('create')
@@ -324,8 +323,8 @@ RSpec.describe 'Materials API', type: :request do
end
it 'returns 422 when the existing tag is not material/character' do
general_tag_name = create(:tag_name, name: 'material_create_general_tag')
create(:tag, primary_tag_name: general_tag_name, category: :general)
general_tag_name = TagName.create!(name: 'material_create_general_tag')
Tag.create!(tag_name: general_tag_name, category: :general)
post '/materials', params: {
tag: 'material_create_general_tag',
@@ -346,7 +345,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:created)
material = Material.order(:id).last
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_url_only')
expect(material.tag.name).to eq('material_create_url_only')
expect(material.url).to eq('https://example.com/material-source')
expect(material.file.attached?).to be(false)
end
@@ -385,7 +384,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'PUT /materials/:id' do
let!(:tag) do
create(:tag, primary_name: 'material_update_old', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'material_update_old'), category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
@@ -464,7 +463,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:ok)
material.reload
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_new')
expect(material.tag.name).to eq('material_update_new')
expect(material.tag.category).to eq('material')
expect(material.url).to eq('https://example.com/updated-source')
expect(material.updated_by_user).to eq(member_user)
@@ -498,8 +497,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:ok)
tag = TagName.find_by!(name: 'material_update_versioned_tag',
language_code: 'ja', primary_flg: true).tag
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_update_versioned_tag' })
version = tag.tag_versions.order(:version_no).last
expect(version.event_type).to eq('create')
@@ -510,7 +508,7 @@ RSpec.describe 'Materials API', type: :request do
it 'backfills a create tag_version for an existing material tag without history' do
existing_tag =
create(:tag, primary_name: 'material_update_existing_no_history',
Tag.create!(tag_name: TagName.create!(name: 'material_update_existing_no_history'),
category: :material)
expect(existing_tag.tag_versions).to be_empty
@@ -533,7 +531,7 @@ RSpec.describe 'Materials API', type: :request do
it 'backfills a create tag_version for an existing character tag without history' do
existing_tag =
create(:tag, primary_name: 'material_update_character_no_history',
Tag.create!(tag_name: TagName.create!(name: 'material_update_character_no_history'),
category: :character)
expect(existing_tag.tag_versions).to be_empty
@@ -565,7 +563,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:ok)
material.reload
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_remove_file')
expect(material.tag.name).to eq('material_update_remove_file')
expect(material.url).to eq('https://example.com/updated-source')
expect(material.updated_by_user).to eq(member_user)
expect(material.file.attached?).to be(false)
@@ -653,8 +651,8 @@ RSpec.describe 'Materials API', type: :request do
end
describe 'GET /materials/download.zip' do
let!(:tag_a) { create(:tag, primary_name: 'zip_a', category: :material) }
let!(:tag_b) { create(:tag, primary_name: 'zip_b', category: :material) }
let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'zip_a'), category: :material) }
let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'zip_b'), category: :material) }
let!(:material_a) do
build_material(tag: tag_a, user: member_user,
file: dummy_upload(filename: 'a.png', body: 'zip-a'))
@@ -703,7 +701,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials/versions' do
let!(:tag) do
create(:tag, primary_name: 'material_history', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'material_history'), category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'history.png'))
@@ -778,7 +776,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'DELETE /materials/:id' do
let!(:tag) do
create(:tag, primary_name: 'material_destroy', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
+9 -9
ファイルの表示
@@ -44,10 +44,10 @@ RSpec.describe 'NicoTags', type: :request do
unlinked.update!(name: 'search_unlinked')
other = create(:external_tag)
other.update!(name: 'other')
destination = create(:tag, category: :general)
primary_tag_name_for(destination, 'ja').update!(name: 'destination_search')
destination = create(:tag, :general)
destination.tag_name.update!(name: 'destination_search')
NicoTagRelation.create!(nico_tag: linked, tag: destination)
NicoTagRelation.create!(nico_tag: other, tag: create(:tag, category: :general))
NicoTagRelation.create!(nico_tag: other, tag: create(:tag, :general))
get '/tags/nico', params: {
name: 'search_',
@@ -151,7 +151,7 @@ RSpec.describe 'NicoTags', type: :request do
it '404 when only an internal tag exists for the target id' do
sign_in_as(member)
non_nico = create(:tag, category: :general)
non_nico = create(:tag, :general)
expect(ExternalTag.exists?(non_nico.id)).to be(false)
patch "/tags/nico/#{non_nico.id}", params: { tags: 'a b' }
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')
linked_a_name = create(:tag_name, name: 'nico_linked_a')
linked_a = create(:tag, primary_tag_name: linked_a_name, category: :general)
linked_a_name = TagName.create!(name: 'nico_linked_a')
linked_a = Tag.create!(tag_name: linked_a_name, category: :general)
linked_b_name = create(:tag_name, name: 'nico_linked_b')
linked_b = create(:tag, primary_tag_name: linked_b_name, category: :general)
linked_b_name = TagName.create!(name: 'nico_linked_b')
linked_b = Tag.create!(tag_name: linked_b_name, category: :general)
NicoTagVersionRecorder.record!(external_tag: nico_tag,
event_type: :create, created_by_user: admin)
expect {
patch "/tags/nico/#{nico_tag.id}", params: {
tags: " #{ linked_a_name.name }\n#{ linked_b_name.name } "
tags: " #{linked_a.name}\n#{linked_b.name} "
}
}.to change(TagVersion, :count).by(2)
.and change(NicoTagVersion, :count).by(1)
+79 -97
ファイルの表示
@@ -113,8 +113,8 @@ RSpec.describe 'Posts API', type: :request do
'sections' => [])
end
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
let!(:post_record) do
Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
@@ -125,11 +125,11 @@ RSpec.describe 'Posts API', type: :request do
describe 'GET /posts' do
let!(:user) { create_member_user! }
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
let!(:tag_name2) { create(:tag_name, name: 'unko') }
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :deerjikist) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name:, category: :general) }
let!(:tag_name2) { TagName.create!(name: 'unko') }
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) }
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
let!(:hit_post) do
Post.create!(uploaded_user: user, title: 'hello spec world',
@@ -267,7 +267,7 @@ RSpec.describe 'Posts API', type: :request do
end
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(json.fetch('count')).to eq(3)
@@ -286,7 +286,7 @@ RSpec.describe 'Posts API', type: :request do
[nil, 'all'].each do |match|
it "intersects internal and external matches with match=#{ match || 'omitted' }" do
params = { tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external" }
params = { tags: "#{ tag.name } nico:search_external" }
params[:match] = match if match
get '/posts', params: params
@@ -307,8 +307,7 @@ RSpec.describe 'Posts API', type: :request do
end
it 'excludes external matches from an internal tag search' do
get '/posts', params: {
tags: "#{ primary_tag_name_for(tag, 'ja').name } not:nico:search_external" }
get '/posts', params: { tags: "#{ tag.name } not:nico:search_external" }
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(2)
@@ -317,9 +316,7 @@ RSpec.describe 'Posts API', type: :request do
end
it 'unions an external match with a negated internal match' do
get '/posts', params: {
tags: "nico:search_external not:#{ primary_tag_name_for(tag, 'ja').name }",
match: 'any' }
get '/posts', params: { tags: "nico:search_external not:#{ tag.name }", match: 'any' }
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(2)
@@ -336,9 +333,7 @@ RSpec.describe 'Posts API', type: :request do
end
it 'applies the same mixed name search to the existing random endpoint' do
get '/posts/random', params: {
tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external",
match: 'all' }
get '/posts/random', params: { tags: "#{ tag.name } nico:search_external", match: 'all' }
expect(response).to have_http_status(:ok)
expect(json.fetch('id')).to eq(both_post.id)
@@ -346,17 +341,17 @@ RSpec.describe 'Posts API', type: :request do
end
context 'when tags contain not:' do
let!(:foo_tag_name) { create(:tag_name, name: 'not_spec_foo') }
let!(:foo_tag) { create(:tag, primary_tag_name: foo_tag_name, category: :general) }
let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') }
let!(:foo_tag) { Tag.create!(tag_name: foo_tag_name, category: :general) }
let!(:bar_tag_name) { create(:tag_name, name: 'not_spec_bar') }
let!(:bar_tag) { create(:tag, primary_tag_name: bar_tag_name, category: :general) }
let!(:bar_tag_name) { TagName.create!(name: 'not_spec_bar') }
let!(:bar_tag) { Tag.create!(tag_name: bar_tag_name, category: :general) }
let!(:baz_tag_name) { create(:tag_name, name: 'not_spec_baz') }
let!(:baz_tag) { create(:tag, primary_tag_name: baz_tag_name, category: :general) }
let!(:baz_tag_name) { TagName.create!(name: 'not_spec_baz') }
let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) }
let!(:foo_alias_tag_name) do
create(:tag_name, :alias, name: 'not_spec_foo_alias', tag: foo_tag)
TagName.create!(name: 'not_spec_foo_alias', canonical: foo_tag_name)
end
let!(:foo_only_post) do
@@ -744,9 +739,7 @@ RSpec.describe 'Posts API', type: :request do
.find { _1['name'] == 'nico:post_index_external' }
expect(post_json.fetch('tags')).to include(
a_hash_including('id' => tag.id,
'name' => primary_tag_name_for(tag, 'ja').name,
'category' => 'general'))
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'))
expect(external_json).not_to be_nil
expect_external_tag_json(external_json, external_tag)
end
@@ -779,10 +772,11 @@ RSpec.describe 'Posts API', type: :request do
end
it 'omits deprecated tags' do
deprecated_tag = create(:tag,
primary_name: 'deprecated_post_tag',
deprecated_tag = Tag.create!(
name: 'deprecated_post_tag',
category: :general,
deprecated_at: Time.current)
deprecated_at: Time.current
)
PostTag.create!(post: post_record, tag: deprecated_tag)
request
@@ -859,9 +853,9 @@ RSpec.describe 'Posts API', type: :request do
tags =
15.times.map do |i|
tag_name = create(:tag_name, name: "show_query_tag_#{ i }")
tag = create(:tag, primary_tag_name: tag_name, category: :general)
create(:tag_name, :alias, name: "show_query_alias_#{ i }", tag:)
tag_name = TagName.create!(name: "show_query_tag_#{ i }")
tag = Tag.create!(tag_name:, category: :general)
TagName.create!(name: "show_query_alias_#{ i }", canonical: tag_name)
PostTag.create!(post: post_record, tag:)
tag
end
@@ -916,9 +910,7 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:ok)
expect(json.fetch('tags')).to include(
a_hash_including('id' => tag.id,
'name' => primary_tag_name_for(tag, 'ja').name,
'category' => 'general'))
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'))
external_json =
json
.fetch('tags')
@@ -1008,7 +1000,7 @@ RSpec.describe 'Posts API', type: :request do
describe 'POST /posts' do
let(:member) { create(:user, :member) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
it '401 when not logged in' do
sign_out
@@ -1165,10 +1157,11 @@ RSpec.describe 'Posts API', type: :request do
end
it 'rejects a deprecated tag specified directly' do
create(:tag,
primary_name: 'deprecated_direct_tag',
Tag.create!(
name: 'deprecated_direct_tag',
category: :general,
deprecated_at: Time.current)
deprecated_at: Time.current
)
sign_in_as(member)
post '/posts', params: post_write_params(
@@ -1185,16 +1178,18 @@ RSpec.describe 'Posts API', type: :request do
end
it 'expands through multiple deprecated parent tags and saves active ancestors' do
child = create(:tag, primary_name: 'active_child', category: :general)
deprecated_parent = create(:tag,
primary_name: 'deprecated_parent',
child = Tag.create!(name: 'active_child', category: :general)
deprecated_parent = Tag.create!(
name: 'deprecated_parent',
category: :general,
deprecated_at: Time.current)
deprecated_grandparent = create(:tag,
primary_name: 'deprecated_grandparent',
deprecated_at: Time.current
)
deprecated_grandparent = Tag.create!(
name: 'deprecated_grandparent',
category: :general,
deprecated_at: Time.current)
active_grandparent = create(:tag, primary_name: 'active_grandparent', category: :general)
deprecated_at: Time.current
)
active_grandparent = Tag.create!(name: 'active_grandparent', category: :general)
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_grandparent)
@@ -1208,8 +1203,7 @@ RSpec.describe 'Posts API', type: :request do
)
expect(response).to have_http_status(:created)
saved_names = TagName.where(tag_id: Post.find(json.fetch('id')).tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
saved_names = Post.find(json.fetch('id')).tags.map(&:name)
expect(saved_names).to include('active_child', 'active_grandparent')
expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent')
end
@@ -1312,8 +1306,7 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = TagName.find_by!(name: '伊地知ニジカ',
language_code: 'ja', primary_flg: true).tag
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
section = PostTagSection.find_by!(post: created_post, tag:)
expect(section.begin_ms).to eq(60_000)
@@ -1333,8 +1326,7 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = TagName.find_by!(name: '伊地知ニジカ',
language_code: 'ja', primary_flg: true).tag
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
end
@@ -1351,8 +1343,7 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = TagName.find_by!(name: '伊地知ニジカ',
language_code: 'ja', primary_flg: true).tag
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
end
@@ -1631,7 +1622,6 @@ RSpec.describe 'Posts API', type: :request do
expect(arguments[:thumbnails].keys).to eq([0])
expect(arguments[:host]).to eq('http://www.example.com')
end
expect(creator).to have_received(:run).with(Locale.nipponese)
end
it 'rejects malformed manifests as a request-level error' do
@@ -1668,8 +1658,8 @@ RSpec.describe 'Posts API', type: :request do
create(:post_tag_section, post: post_record, tag:,
begin_ms: 1000, end_ms: 2000)
tn2 = create(:tag_name, name: 'spec_tag_2')
replacement_tag = create(:tag, primary_tag_name: tn2, category: :general)
tn2 = TagName.create!(name: 'spec_tag_2')
replacement_tag = Tag.create!(tag_name: tn2, category: :general)
put "/posts/#{post_record.id}", params: post_update_params(
post_record,
@@ -1717,10 +1707,11 @@ RSpec.describe 'Posts API', type: :request do
end
it 'rejects a deprecated tag specified directly' do
create(:tag,
primary_name: 'deprecated_update_tag',
Tag.create!(
name: 'deprecated_update_tag',
category: :general,
deprecated_at: Time.current)
deprecated_at: Time.current
)
sign_in_as(member)
put "/posts/#{ post_record.id }", params: post_update_params(
@@ -1975,7 +1966,7 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: 'updated by me',
tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}")
tags: "spec_tag #{Tag.no_deerjikist.name}")
expect(response).to have_http_status(:conflict)
@@ -1995,8 +1986,7 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload)
current_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'current_added_tag', category: :general)
current_tag = Tag.find_or_create_by_tag_name!('current_added_tag', category: :general)
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
PostVersionRecorder.record!(
@@ -2007,8 +1997,7 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
'incoming_added_tag'].join(' '))
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_added_tag")
expect(response).to have_http_status(:conflict)
@@ -2026,8 +2015,7 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload)
current_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'current_merge_tag', category: :general)
current_tag = Tag.find_or_create_by_tag_name!('current_merge_tag', category: :general)
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
PostVersionRecorder.record!(
@@ -2038,17 +2026,15 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
'incoming_merge_tag'].join(' '),
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_merge_tag",
merge: '1')
expect(response).to have_http_status(:ok)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
names = post_record.reload.tags.map(&:name)
expect(names).to include('spec_tag')
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(names).to include(Tag.no_deerjikist.name)
expect(names).to include('current_merge_tag')
expect(names).to include('incoming_merge_tag')
end
@@ -2071,15 +2057,14 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
tags: "spec_tag #{ Tag.no_deerjikist.name }")
expect(response).to have_http_status(:ok)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
names = post_record.reload.tags.map(&:name)
expect(names).to include('spec_tag')
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(names).to include(Tag.no_deerjikist.name)
expect(post_record.external_tags).to contain_exactly(nico_tag)
end
@@ -2094,15 +2079,14 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: 'updated title',
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
tags: "spec_tag #{ Tag.no_deerjikist.name }")
expect(response).to have_http_status(:ok)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
names = post_record.reload.tags.map(&:name)
expect(names).to include('spec_tag')
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(names).to include(Tag.no_deerjikist.name)
expect(post_record.external_tags).to contain_exactly(nico_tag)
external_json =
@@ -2118,8 +2102,7 @@ RSpec.describe 'Posts API', type: :request do
sign_in_as(member)
nico_tag = create_nico_tag!('nico:relation_source')
linked_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'relation_linked_tag', category: :general)
linked_tag = Tag.find_or_create_by_tag_name!('relation_linked_tag', category: :general)
NicoTagRelation.create!(nico_tag:, tag: linked_tag)
PostExternalTag.create!(post: post_record, external_tag: nico_tag)
@@ -2130,17 +2113,16 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
tags: "spec_tag #{ Tag.no_deerjikist.name }")
expect(response).to have_http_status(:ok)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
names = post_record.reload.tags.map(&:name)
expect(post_record.external_tags).to contain_exactly(nico_tag)
expect(names).to include('spec_tag')
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(names).not_to include(primary_tag_name_for(linked_tag, 'ja').name)
expect(names).to include(Tag.no_deerjikist.name)
expect(names).not_to include(linked_tag.name)
end
it 'force-updates stale posts without base_version_no' do
@@ -2156,7 +2138,7 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
title: 'forced title',
tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}",
tags: "spec_tag #{Tag.no_deerjikist.name}",
force: '1')
expect(response).to have_http_status(:ok)
@@ -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_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) }
let!(:tag_name2) { create(:tag_name, name: 'spec_tag_2') }
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :general) }
let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') }
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) }
def snapshot_tags(post)
post.snapshot_tag_names.join(' ')
@@ -2435,7 +2417,7 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(3)
expect(json.fetch('versions').first.fetch('tags')).to include(
'name' => primary_tag_name_for(tag2, 'ja').name, 'type' => 'context')
'name' => tag2.name, 'type' => 'context')
end
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)
base_version = create_post_version_for!(post_record)
tag_name2 = create(:tag_name, name: 'spec_tag_2')
create(:tag, primary_tag_name: tag_name2, category: :general)
tag_name2 = TagName.create!(name: 'spec_tag_2')
Tag.create!(tag_name: tag_name2, category: :general)
expect do
put "/posts/#{post_record.id}", params: post_write_params(
@@ -2733,8 +2715,8 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload)
tag_name2 = create(:tag_name, name: 'spec_tag_2')
tag2 = create(:tag, primary_tag_name: tag_name2, category: :general)
tag_name2 = TagName.create!(name: 'spec_tag_2')
tag2 = Tag.create!(tag_name: tag_name2, category: :general)
expect {
put "/posts/#{post_record.id}", params: post_write_params(
+9 -15
ファイルの表示
@@ -3,13 +3,13 @@ require 'rails_helper'
RSpec.describe 'TagVersions API', type: :request do
let(:member) { create(:user, :member, name: 'version member') }
let!(:tag) { create(:tag, primary_name: 'tag_versions_target', category: :general) }
let!(:other_tag) { create(:tag, primary_name: 'tag_versions_other', category: :general) }
let!(:tag) { create(:tag, name: 'tag_versions_target', category: :general) }
let!(:other_tag) { create(:tag, name: 'tag_versions_other', category: :general) }
let!(:parent_shared) { create(:tag, primary_name: 'parent_shared', category: :general) }
let!(:parent_old) { create(:tag, primary_name: 'parent_old', category: :general) }
let!(:parent_new) { create(:tag, primary_name: 'parent_new', category: :general) }
let!(:other_parent) { create(:tag, primary_name: 'other_parent', category: :general) }
let!(:parent_shared) { create(:tag, name: 'parent_shared', category: :general) }
let!(:parent_old) { create(:tag, name: 'parent_old', category: :general) }
let!(:parent_new) { create(:tag, name: 'parent_new', category: :general) }
let!(:other_parent) { create(:tag, name: 'other_parent', category: :general) }
let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) }
let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) }
@@ -36,12 +36,6 @@ RSpec.describe 'TagVersions API', type: :request do
category: category,
deprecated_at: deprecated_at,
aliases: Array(aliases).join(' '),
names: [{ language_code: 'ja', name:, script_code: 'Jpan',
primary_flg: true,
auto_generated: false }] + Array(aliases).map { |alias_name|
{ language_code: 'ja', name: alias_name, script_code: 'Jpan',
primary_flg: false, auto_generated: false }
},
parent_tag_ids: Array(parent_tags).map(&:id).join(' '),
created_by_user: created_by_user,
created_at: created_at)
@@ -217,7 +211,7 @@ RSpec.describe 'TagVersions API', type: :request do
end
it 'returns empty when the specified tag has no versions' do
fresh_tag = create(:tag, primary_name: 'no_versions_tag', category: :general)
fresh_tag = create(:tag, name: 'no_versions_tag', category: :general)
get '/tags/versions', params: { id: fresh_tag.id }
@@ -238,8 +232,8 @@ RSpec.describe 'TagVersions API', type: :request do
end
it 'does not create tag versions by wiki updates when tag has no versions yet' do
wiki_tag_name = create(:tag_name, name: 'tag_versions_from_wiki')
wiki_tag = create(:tag, primary_tag_name: wiki_tag_name, category: :general)
wiki_tag_name = TagName.create!(name: 'tag_versions_from_wiki')
wiki_tag = Tag.create!(tag_name: wiki_tag_name, category: :general)
wiki_page =
Wiki::Commit.create_content!(
+7 -7
ファイルの表示
@@ -8,13 +8,13 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
end
def create_tag! name:, category: :general
tag_name = create(:tag_name, name:)
create(:tag, primary_tag_name: tag_name, category:)
tag_name = TagName.create!(name:)
Tag.create!(tag_name:, category:)
end
def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body:,
created_by_user: user,
message: 'init')
@@ -43,7 +43,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
wiki_page.reload
version = wiki_page.wiki_versions.order(:version_no).last
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after')
expect(tag.name).to eq('patch_tag_wiki_after')
expect(wiki_page.title).to eq('patch_tag_wiki_after')
expect(version).to have_attributes(
@@ -72,7 +72,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
tag.reload
wiki_page.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_category_only')
expect(tag.name).to eq('patch_tag_category_only')
expect(tag.category).to eq('meme')
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
end
@@ -101,7 +101,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
wiki_page.reload
version = wiki_page.wiki_versions.order(:version_no).last
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after')
expect(tag.name).to eq('put_tag_wiki_after')
expect(wiki_page.title).to eq('put_tag_wiki_after')
expect(version).to have_attributes(
@@ -134,7 +134,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
tag.reload
wiki_page.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_category_only')
expect(tag.name).to eq('put_tag_category_only')
expect(tag.category).to eq('meme')
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
end
+2 -2
ファイルの表示
@@ -12,7 +12,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
let(:guest) { create(:user, role: :guest) }
before do
primary_tag_name_for(tag, 'ja').update!(name: 'deerjika')
tag.tag_name.update!(name: 'deerjika')
end
describe 'GET /tags/:id/deerjikists' do
@@ -344,7 +344,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
end
before do
primary_tag_name_for(other_tag, 'ja').update!(name: 'existing-deerjikist')
other_tag.tag_name.update!(name: 'existing-deerjikist')
end
it 'returns an indexed 422 error and rolls back the complete replacement' do
+216 -236
ファイルの表示
@@ -2,13 +2,13 @@ require 'cgi'
require 'rails_helper'
RSpec.describe 'Tags API', type: :request do
let!(:tn) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tn, category: :general) }
let!(:alias_tn) { create(:tag_name, :alias, name: 'unko', tag:) }
let!(:tn) { TagName.create!(name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name: tn, category: :general) }
let!(:alias_tn) { TagName.create!(name: 'unko', canonical: tn) }
let!(:post) { Post.create!(url: 'https://example.com/unkounkounko') }
let!(:post_tag) { PostTag.create!(post:, tag:) }
let!(:tn2) { create(:tag_name, name: 'unknown') }
let!(:tag2) { create(:tag, primary_tag_name: tn2, category: :general) }
let!(:tn2) { TagName.create!(name: 'unknown') }
let!(:tag2) { Tag.create!(tag_name: tn2, category: :general) }
def response_tags
json.fetch('tags')
@@ -18,18 +18,6 @@ RSpec.describe 'Tags API', type: :request do
response_tags.map { |t| t.fetch('name') }
end
def version_primary_name(version, language_code)
version.names.find { |name|
name['language_code'] == language_code && name['primary_flg']
}.fetch('name')
end
def version_alias_names(version, language_code)
version.names.filter_map { |name|
name['name'] if name['language_code'] == language_code && !name['primary_flg']
}
end
def dummy_material_upload(filename: 'dummy.png', type: 'image/png', body: 'dummy')
Rack::Test::UploadedFile.new(StringIO.new(body), type, original_filename: filename)
end
@@ -50,9 +38,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(2)
expect(response_tags).to contain_exactly(
a_hash_including('id' => tag.id,
'name' => primary_tag_name_for(tag, 'ja').name,
'category' => 'general'),
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'),
a_hash_including(
'id' => external.id,
'name' => 'nico:spec_external',
@@ -105,7 +91,7 @@ RSpec.describe 'Tags API', type: :request do
end
it 'filters tags by category' do
meme = create(:tag, primary_name: 'meme_only', category: :meme)
meme = Tag.create!(tag_name: TagName.create!(name: 'meme_only'), category: :meme)
get '/tags', params: { category: 'meme' }
@@ -115,29 +101,30 @@ RSpec.describe 'Tags API', type: :request do
end
it 'filters tags by deprecated state' do
deprecated_tag = create(:tag,
primary_name: 'deprecated_filter',
deprecated_tag = Tag.create!(
name: 'deprecated_filter',
category: :general,
deprecated_at: 1.day.from_now)
active_tag = create(:tag, primary_name: 'active_filter', category: :general)
deprecated_at: 1.day.from_now
)
active_tag = Tag.create!(name: 'active_filter', category: :general)
get '/tags', params: { name: '_filter', deprecated: '1' }
expect(response).to have_http_status(:ok)
expect(response_names).to include(primary_tag_name_for(deprecated_tag, 'ja').name)
expect(response_names).not_to include(primary_tag_name_for(active_tag, 'ja').name)
expect(response_names).to include(deprecated_tag.name)
expect(response_names).not_to include(active_tag.name)
get '/tags', params: { name: '_filter', deprecated: '0' }
expect(response).to have_http_status(:ok)
expect(response_names).to include(primary_tag_name_for(active_tag, 'ja').name)
expect(response_names).not_to include(primary_tag_name_for(deprecated_tag, 'ja').name)
expect(response_names).to include(active_tag.name)
expect(response_names).not_to include(deprecated_tag.name)
end
it 'filters tags by post_count range' do
low = create(:tag, primary_name: 'pc_low', category: :general)
mid = create(:tag, primary_name: 'pc_mid', category: :general)
high = create(:tag, primary_name: 'pc_high', category: :general)
low = Tag.create!(tag_name: TagName.create!(name: 'pc_low'), category: :general)
mid = Tag.create!(tag_name: TagName.create!(name: 'pc_mid'), category: :general)
high = Tag.create!(tag_name: TagName.create!(name: 'pc_high'), category: :general)
low.update_columns(post_count: 1)
mid.update_columns(post_count: 3)
@@ -155,8 +142,8 @@ RSpec.describe 'Tags API', type: :request do
end
it 'filters tags by created_at range' do
old_tag = create(:tag, primary_name: 'created_old', category: :general)
new_tag = create(:tag, primary_name: 'created_new', category: :general)
old_tag = Tag.create!(tag_name: TagName.create!(name: 'created_old'), category: :general)
new_tag = Tag.create!(tag_name: TagName.create!(name: 'created_new'), category: :general)
old_time = Time.zone.local(2024, 1, 1, 0, 0, 0)
new_time = Time.zone.local(2024, 2, 1, 0, 0, 0)
@@ -175,8 +162,8 @@ RSpec.describe 'Tags API', type: :request do
end
it 'filters tags by updated_at range' do
old_tag = create(:tag, primary_name: 'updated_old', category: :general)
new_tag = create(:tag, primary_name: 'updated_new', category: :general)
old_tag = Tag.create!(tag_name: TagName.create!(name: 'updated_old'), category: :general)
new_tag = Tag.create!(tag_name: TagName.create!(name: 'updated_new'), category: :general)
old_time = Time.zone.local(2024, 3, 1, 0, 0, 0)
new_time = Time.zone.local(2024, 4, 1, 0, 0, 0)
@@ -195,12 +182,12 @@ RSpec.describe 'Tags API', type: :request do
end
it 'orders tags by custom category order' do
create(:tag, primary_name: 'cat_deerjikist', category: :deerjikist)
create(:tag, primary_name: 'cat_meme', category: :meme)
create(:tag, primary_name: 'cat_character', category: :character)
create(:tag, primary_name: 'cat_general', category: :general)
create(:tag, primary_name: 'cat_material', category: :material)
create(:tag, primary_name: 'cat_meta', category: :meta)
Tag.create!(tag_name: TagName.create!(name: 'cat_deerjikist'), category: :deerjikist)
Tag.create!(tag_name: TagName.create!(name: 'cat_meme'), category: :meme)
Tag.create!(tag_name: TagName.create!(name: 'cat_character'), category: :character)
Tag.create!(tag_name: TagName.create!(name: 'cat_general'), category: :general)
Tag.create!(tag_name: TagName.create!(name: 'cat_material'), category: :material)
Tag.create!(tag_name: TagName.create!(name: 'cat_meta'), category: :meta)
create(:external_tag, name: 'cat_nico')
get '/tags', params: { name: 'cat_', order: 'category:asc', limit: 20 }
@@ -215,19 +202,19 @@ RSpec.describe 'Tags API', type: :request do
context 'with mixed legacy pagination' do
let!(:first_tag) do
create(:tag,
primary_tag_name: create(:tag_name, name: 'a_mixed_page'),
tag_name: create(:tag_name, name: 'a_mixed_page'),
category: :meme)
end
let!(:middle_tag) do
create(:tag,
primary_tag_name: create(:tag_name, name: 'm_mixed_page'),
tag_name: create(:tag_name, name: 'm_mixed_page'),
category: :meta)
end
let!(:last_tag) do
create(:tag,
primary_tag_name: create(:tag_name, name: 'z_mixed_page'),
tag_name: create(:tag_name, name: 'z_mixed_page'),
category: :general)
end
@@ -278,7 +265,7 @@ RSpec.describe 'Tags API', type: :request do
it 'paginates and keeps total count' do
%w[pag_a pag_b pag_c].each do |name|
create(:tag, primary_tag_name: create(:tag_name, name:), category: :general)
Tag.create!(tag_name: TagName.create!(name:), category: :general)
end
get '/tags', params: { name: 'pag_', order: 'name:asc', page: 2, limit: 2 }
@@ -289,8 +276,8 @@ RSpec.describe 'Tags API', type: :request do
end
it 'falls back to default ordering when order is invalid' do
low = create(:tag, primary_name: 'fallback_low', category: :general)
high = create(:tag, primary_name: 'fallback_high', category: :general)
low = Tag.create!(tag_name: TagName.create!(name: 'fallback_low'), category: :general)
high = Tag.create!(tag_name: TagName.create!(name: 'fallback_high'), category: :general)
low.update_columns(post_count: 1)
high.update_columns(post_count: 9)
@@ -303,7 +290,7 @@ RSpec.describe 'Tags API', type: :request do
it 'normalises invalid page and limit' do
%w[norm_a norm_b].each do |name|
create(:tag, primary_tag_name: create(:tag_name, name:), category: :general)
Tag.create!(tag_name: TagName.create!(name:), category: :general)
end
get '/tags', params: { name: 'norm_', order: 'name:asc', page: 0, limit: 0 }
@@ -315,9 +302,10 @@ RSpec.describe 'Tags API', type: :request do
end
it 'returns aliases and parent tags' do
parent_tag = create(:tag,
primary_tag_name: create(:tag_name, name: 'index_parent_tag'),
category: :meme)
parent_tag = Tag.create!(
tag_name: TagName.create!(name: 'index_parent_tag'),
category: :meme
)
TagImplication.create!(tag:, parent_tag:)
get '/tags', params: { name: 'spec_tag' }
@@ -361,9 +349,10 @@ RSpec.describe 'Tags API', type: :request do
end
it 'returns aliases and parent tags' do
parent_tag = create(:tag,
primary_tag_name: create(:tag_name, name: 'show_parent_tag'),
category: :character)
parent_tag = Tag.create!(
tag_name: TagName.create!(name: 'show_parent_tag'),
category: :character
)
TagImplication.create!(tag:, parent_tag:)
request
@@ -409,7 +398,7 @@ RSpec.describe 'Tags API', type: :request do
it 'prefers Tag when Tag and ExternalTag have the same id' do
internal = create(
:tag,
primary_tag_name: create(:tag_name, name: 'internal_collision'))
tag_name: create(:tag_name, name: 'internal_collision'))
create(
:external_tag,
id: internal.id,
@@ -427,7 +416,7 @@ RSpec.describe 'Tags API', type: :request do
describe 'GET /tags/autocomplete' do
it 'combines internal and external matches without conflating equal ids' do
internal = create(:tag, category: :general, primary_name: 'mixed_internal', post_count: 2)
internal = Tag.create!(category: :general, name: 'mixed_internal', post_count: 2)
external = create(:external_tag, id: internal.id,
name: 'mixed_external', post_count: 3)
@@ -466,7 +455,7 @@ RSpec.describe 'Tags API', type: :request do
it 'limits the combined results to 20 and sorts ties by displayed name' do
11.times do |i|
name = "combined_#{ i.to_s.rjust(2, '0') }"
create(:tag, category: :general, primary_name: name, post_count: 1)
Tag.create!(category: :general, name:, post_count: 1)
create(:external_tag, name:, post_count: 1)
end
@@ -478,39 +467,34 @@ RSpec.describe 'Tags API', type: :request do
end
it 'excludes external tags but preserves internal alias matches when nico is false' do
internal = create(:tag, category: :general, primary_name: 'switch_internal', post_count: 1)
alias_target = create(:tag, category: :general, primary_name: 'alias_target', post_count: 1)
create(:tag_name, :alias, name: 'switch_alias', tag: alias_target)
internal = Tag.create!(category: :general, name: 'switch_internal', post_count: 1)
alias_target = Tag.create!(category: :general, name: 'alias_target', post_count: 1)
TagName.create!(name: 'switch_alias', canonical: alias_target.tag_name)
create(:external_tag, name: 'switch_external', post_count: 1)
get '/tags/autocomplete', params: { q: 'switch', nico: '0' }
expect(response).to have_http_status(:ok)
expect(json).to contain_exactly(
a_hash_including('id' => internal.id,
'name' => primary_tag_name_for(internal, 'ja').name),
a_hash_including('id' => alias_target.id,
'name' => primary_tag_name_for(alias_target, 'ja').name,
a_hash_including('id' => internal.id, 'name' => internal.name),
a_hash_including('id' => alias_target.id, 'name' => alias_target.name,
'matched_alias' => 'switch_alias'))
end
['%', '_'].each do |wildcard|
it "treats #{ wildcard } literally for primary, alias, and external names" do
it "treats #{ wildcard } literally for canonical, alias, and external names" do
literal = "literal#{ wildcard }match"
create(:tag, category: :general, primary_name: literal, post_count: 1)
alias_target = create(
:tag, category: :general, primary_name: 'literal_alias_target', post_count: 1)
create(:tag_name, :alias, name: "#{ literal }_alias",
tag: alias_target)
Tag.create!(category: :general, name: literal, post_count: 1)
alias_target = Tag.create!(category: :general, name: 'literal_alias_target', post_count: 1)
TagName.create!(name: "#{ literal }_alias", canonical: alias_target.tag_name)
create(:external_tag, name: literal, post_count: 1)
create(:tag, category: :general, primary_name: 'literalXmatch', post_count: 2)
Tag.create!(category: :general, name: 'literalXmatch', post_count: 2)
create(:external_tag, name: 'literalXmatch', post_count: 2)
get '/tags/autocomplete', params: { q: "literal#{ wildcard }" }
expect(json.map { |row| row.fetch('name') })
.to contain_exactly(
literal, primary_tag_name_for(alias_target, 'ja').name, "nico:#{ literal }")
.to contain_exactly(literal, alias_target.name, "nico:#{ literal }")
end
end
@@ -526,7 +510,7 @@ RSpec.describe 'Tags API', type: :request do
expect(t['matched_alias']).to be(nil)
end
it 'returns matching primary tag names by q with aliases' do
it 'returns matching canonical tags by q with aliases' do
get '/tags/autocomplete', params: { q: 'unk' }
expect(response).to have_http_status(:ok)
@@ -539,10 +523,11 @@ RSpec.describe 'Tags API', type: :request do
end
it 'omits deprecated tags' do
deprecated_tag = create(:tag,
primary_name: 'spec_deprecated',
deprecated_tag = Tag.create!(
name: 'spec_deprecated',
category: :general,
deprecated_at: Time.current)
deprecated_at: Time.current
)
deprecated_tag.update_columns(post_count: 1)
get '/tags/autocomplete', params: { q: 'spec_', present: '0' }
@@ -636,7 +621,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('new')
expect(tag.name).to eq('new')
expect(tag.category).to eq('general')
body = JSON.parse(response.body)
@@ -651,7 +636,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(tag.name).to eq('spec_tag')
expect(tag.category).to eq('meme')
end
@@ -661,7 +646,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(tag.name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -671,7 +656,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('n')
expect(tag.name).to eq('n')
expect(tag.category).to eq('meta')
end
@@ -684,7 +669,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' }
expect(response).to have_http_status(:unprocessable_entity)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.reload.name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -699,11 +684,11 @@ RSpec.describe 'Tags API', type: :request do
expect(versions.map(&:event_type)).to eq(['create', 'update'])
expect(version_primary_name(versions.first, 'ja')).to eq('spec_tag')
expect(versions.first.name).to eq('spec_tag')
expect(versions.first.category).to eq('general')
expect(version_alias_names(versions.first, 'ja')).to include('unko')
expect(versions.first.aliases.split).to include('unko')
expect(version_primary_name(versions.second, 'ja')).to eq('new_tag_name')
expect(versions.second.name).to eq('new_tag_name')
expect(versions.second.category).to eq('meme')
expect(versions.second.created_by_user_id).to eq(member_user.id)
end
@@ -734,7 +719,7 @@ RSpec.describe 'Tags API', type: :request do
it 'PATCH で tag の name を変更すると対応する wiki version を作成する' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -762,7 +747,7 @@ RSpec.describe 'Tags API', type: :request do
it 'tag の category だけを変更しても wiki version は作成しない' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -789,8 +774,7 @@ RSpec.describe 'Tags API', type: :request do
}
expect(response).to have_http_status(:ok)
expect(TagName.find_by!(language_code: 'ja', name: 'spec_tag'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.find_by!(name: 'spec_tag').canonical).to eq(tag.reload.tag_name)
patch "/tags/#{ tag.id }", params: { name: 'spec_tag' }
@@ -798,17 +782,15 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(primary_tag_name_for(tag, 'ja')).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: true)
expect(TagName.find_by!(language_code: 'ja', name: 'patch_roundtrip_target'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(tag.name).to eq('spec_tag')
expect(tag.tag_name.canonical_id).to be_nil
expect(TagName.find_by!(name: 'patch_roundtrip_target').canonical).to eq(tag.tag_name)
end
it '別 tag の正規名には変更できない' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body: 'patch collision wiki',
created_by_user: member_user,
message: 'init')
@@ -820,10 +802,9 @@ RSpec.describe 'Tags API', type: :request do
'name' => ['その名前は既に使はれてゐます.']
)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.to contain_exactly('unko')
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja'))
expect(tag.reload.name).to eq('spec_tag')
expect(tag.tag_name.aliases.map(&:name)).to contain_exactly('unko')
expect(wiki_page.reload.tag_name).to eq(tag.tag_name)
end
it 'system tag の name は変更できない' do
@@ -832,34 +813,34 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{ system_tag.id }", params: { name: 'patch_system_tag_renamed' }
expect(response).to have_http_status(:unprocessable_entity)
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq('bot操作')
expect(system_tag.reload.name).to eq('bot操作')
end
end
end
describe 'GET /tags/with-depth' do
let!(:root_meme) do
create(:tag, primary_name: 'depth_a_root_meme', category: :meme)
Tag.create!(tag_name: TagName.create!(name: 'depth_a_root_meme'), category: :meme)
end
let!(:root_material) do
create(:tag, primary_name: 'depth_b_root_material', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'depth_b_root_material'), category: :material)
end
let!(:hidden_general_root) do
create(:tag, primary_name: 'depth_hidden_general_root', category: :general)
Tag.create!(tag_name: TagName.create!(name: 'depth_hidden_general_root'), category: :general)
end
let!(:child_character) do
create(:tag, primary_name: 'depth_child_character', category: :character)
Tag.create!(tag_name: TagName.create!(name: 'depth_child_character'), category: :character)
end
let!(:grandchild_material) do
create(:tag, primary_name: 'depth_grandchild_material', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'depth_grandchild_material'), category: :material)
end
let!(:child_general) do
create(:tag, primary_name: 'depth_child_general', category: :general)
Tag.create!(tag_name: TagName.create!(name: 'depth_child_general'), category: :general)
end
before do
@@ -902,13 +883,15 @@ RSpec.describe 'Tags API', type: :request do
end
it 'passes through deprecated tags when finding children' do
deprecated_middle = create(:tag,
primary_name: 'depth_deprecated_middle',
deprecated_middle = Tag.create!(
name: 'depth_deprecated_middle',
category: :character,
deprecated_at: Time.current)
visible_descendant = create(:tag,
primary_name: 'depth_visible_descendant',
category: :material)
deprecated_at: Time.current
)
visible_descendant = Tag.create!(
name: 'depth_visible_descendant',
category: :material
)
TagImplication.create!(parent_tag: root_material, tag: deprecated_middle)
TagImplication.create!(parent_tag: deprecated_middle, tag: visible_descendant)
@@ -920,20 +903,24 @@ RSpec.describe 'Tags API', type: :request do
end
it 'passes through multiple deprecated tags for roots and has_children' do
active_child = create(:tag,
primary_name: 'depth_active_child_below_deprecated',
category: :character)
deprecated_parent = create(:tag,
primary_name: 'depth_deprecated_parent',
active_child = Tag.create!(
name: 'depth_active_child_below_deprecated',
category: :character
)
deprecated_parent = Tag.create!(
name: 'depth_deprecated_parent',
category: :character,
deprecated_at: Time.current)
deprecated_grandparent = create(:tag,
primary_name: 'depth_deprecated_grandparent',
deprecated_at: Time.current
)
deprecated_grandparent = Tag.create!(
name: 'depth_deprecated_grandparent',
category: :material,
deprecated_at: Time.current)
active_ancestor = create(:tag,
primary_name: 'depth_active_ancestor',
category: :meme)
deprecated_at: Time.current
)
active_ancestor = Tag.create!(
name: 'depth_active_ancestor',
category: :meme
)
TagImplication.create!(tag: active_child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
@@ -958,13 +945,15 @@ RSpec.describe 'Tags API', type: :request do
end
it 'treats an active tag with only deprecated ancestors as a root' do
active_child = create(:tag,
primary_name: 'depth_root_below_deprecated',
category: :character)
deprecated_parent = create(:tag,
primary_name: 'depth_root_deprecated_parent',
active_child = Tag.create!(
name: 'depth_root_below_deprecated',
category: :character
)
deprecated_parent = Tag.create!(
name: 'depth_root_deprecated_parent',
category: :material,
deprecated_at: Time.current)
deprecated_at: Time.current
)
TagImplication.create!(tag: active_child, parent_tag: deprecated_parent)
get '/tags/with-depth'
@@ -978,14 +967,16 @@ RSpec.describe 'Tags API', type: :request do
end
it 'terminates when deprecated implications contain a cycle' do
first = create(:tag,
primary_name: 'depth_cycle_first',
first = Tag.create!(
name: 'depth_cycle_first',
category: :character,
deprecated_at: Time.current)
second = create(:tag,
primary_name: 'depth_cycle_second',
deprecated_at: Time.current
)
second = Tag.create!(
name: 'depth_cycle_second',
category: :material,
deprecated_at: Time.current)
deprecated_at: Time.current
)
TagImplication.create!(tag: first, parent_tag: root_material)
TagImplication.create!(tag: second, parent_tag: first)
now = Time.current
@@ -1011,19 +1002,19 @@ RSpec.describe 'Tags API', type: :request do
let!(:material_user) { create_member_user! }
let!(:root_tag) do
create(:tag, primary_name: 'materials_root', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'materials_root'), category: :material)
end
let!(:child_a_tag) do
create(:tag, primary_name: 'materials_child_a', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'materials_child_a'), category: :material)
end
let!(:child_b_tag) do
create(:tag, primary_name: 'materials_child_b', category: :character)
Tag.create!(tag_name: TagName.create!(name: 'materials_child_b'), category: :character)
end
let!(:grandchild_tag) do
create(:tag, primary_name: 'materials_grandchild', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'materials_grandchild'), category: :material)
end
let!(:root_material) do
@@ -1045,7 +1036,7 @@ RSpec.describe 'Tags API', type: :request do
end
it 'returns a tag tree with nested materials sorted by child name' do
get "/tags/name/#{ CGI.escape(primary_tag_name_for(root_tag, 'ja').name) }/materials"
get "/tags/name/#{ CGI.escape(root_tag.name) }/materials"
expect(response).to have_http_status(:ok)
@@ -1136,7 +1127,7 @@ RSpec.describe 'Tags API', type: :request do
}
expect(response).to have_http_status(:unprocessable_entity)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.reload.name).to eq('spec_tag')
end
it 'category が空なら 422 を返す' do
@@ -1148,7 +1139,7 @@ RSpec.describe 'Tags API', type: :request do
}
expect(response).to have_http_status(:unprocessable_entity)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.reload.name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -1167,12 +1158,14 @@ RSpec.describe 'Tags API', type: :request do
end
it 'name, category, aliases, parent tags をまとめて更新できる' do
old_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_old_parent'),
category: :general)
kept_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_kept_parent'),
category: :general)
old_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_old_parent'),
category: :general
)
kept_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_kept_parent'),
category: :general
)
TagImplication.create!(tag:, parent_tag: old_parent)
TagImplication.create!(tag:, parent_tag: kept_parent)
@@ -1188,24 +1181,22 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_renamed_tag')
expect(tag.name).to eq('put_renamed_tag')
expect(tag.category).to eq('meme')
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_a'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.find_by(name: 'put_alias_a').canonical).to eq(tag.tag_name)
expect(TagName.find_by(name: 'put_alias_b').canonical).to eq(tag.tag_name)
old_name_alias = TagName.find_by(language_code: 'ja', name: 'spec_tag')
old_name_alias = TagName.find_by(name: 'spec_tag')
expect(old_name_alias).to be_present
expect(old_name_alias).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(old_name_alias.canonical).to eq(tag.tag_name)
expect(alias_tn.reload.tag_id).to be_nil
expect(alias_tn.reload.canonical).to be_nil
expect(TagName.where(tag_id: tag.parents.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.to contain_exactly('put_kept_parent', 'put_new_parent')
expect(tag.parents.map(&:name)).to contain_exactly(
'put_kept_parent',
'put_new_parent'
)
expect(TagImplication.where(tag:, parent_tag: old_parent)).not_to exist
@@ -1235,8 +1226,7 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_self_test'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.find_by(name: 'put_alias_self_test').canonical).to eq(tag.tag_name)
expect(json['aliases']).to include('put_alias_self_test')
expect(json['aliases']).not_to include('spec_tag')
end
@@ -1273,13 +1263,13 @@ RSpec.describe 'Tags API', type: :request do
expect(versions.map(&:event_type)).to eq(['create', 'update'])
expect(version_primary_name(versions.first, 'ja')).to eq('spec_tag')
expect(versions.first.name).to eq('spec_tag')
expect(versions.first.category).to eq('general')
expect(version_alias_names(versions.first, 'ja')).to include('unko')
expect(versions.first.aliases.split).to include('unko')
expect(version_primary_name(versions.second, 'ja')).to eq('put_versioned_tag')
expect(versions.second.name).to eq('put_versioned_tag')
expect(versions.second.category).to eq('meta')
expect(version_alias_names(versions.second, 'ja')).to include('spec_tag')
expect(versions.second.aliases.split).to include('spec_tag')
expect(versions.second.created_by_user_id).to eq(member_user.id)
end
@@ -1306,37 +1296,35 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(TagName.find_by!(language_code: 'ja', name: 'put_roundtrip_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.to contain_exactly('put_roundtrip_b', 'unko')
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.not_to include('spec_tag')
expect(alias_tn.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(tag.name).to eq('spec_tag')
expect(TagName.find_by!(name: 'put_roundtrip_b').canonical).to eq(tag.tag_name)
expect(tag.tag_name.aliases.map(&:name)).to contain_exactly('put_roundtrip_b', 'unko')
expect(tag.tag_name.aliases.map(&:name)).not_to include('spec_tag')
expect(alias_tn.reload.canonical).to eq(tag.tag_name)
version = tag.tag_versions.order(:version_no).last
expect(version.event_type).to eq('update')
expect(version_primary_name(version, 'ja')).to eq('spec_tag')
expect(version_alias_names(version, 'ja')).to contain_exactly('put_roundtrip_b', 'unko')
expect(version.name).to eq('spec_tag')
expect(version.aliases.split).to contain_exactly('put_roundtrip_b', 'unko')
end
it 'parent tag の snapshot も作成する' do
old_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_snapshot_old_parent'),
category: :general)
new_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_snapshot_new_parent'),
category: :general)
old_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_snapshot_old_parent'),
category: :general
)
new_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_snapshot_new_parent'),
category: :general
)
TagImplication.create!(tag:, parent_tag: old_parent)
put "/tags/#{ tag.id }", params: {
name: 'spec_tag',
category: 'general',
aliases: 'unko',
parent_tags: primary_tag_name_for(new_parent, 'ja').name,
parent_tags: new_parent.name,
deprecated: '0',
}
@@ -1359,13 +1347,13 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.reload.name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
it 'system tag の name は変更できない' do
system_tag = Tag.tagme
old_name = primary_tag_name_for(system_tag, 'ja').name
old_name = system_tag.name
old_category = system_tag.category
expect {
@@ -1380,14 +1368,14 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq(old_name)
expect(system_tag.reload.name).to eq(old_name)
expect(system_tag.category).to eq(old_category)
end
it 'wiki を持つ tag を更新すると wiki version も作成する' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -1423,10 +1411,10 @@ RSpec.describe 'Tags API', type: :request do
)
end
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新 canonical へ移す' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -1455,11 +1443,9 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja'))
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b').wiki_page)
.to be_nil
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(wiki_page.reload.tag_name).to eq(tag.tag_name)
expect(TagName.find_by!(name: 'put_wiki_roundtrip_b').wiki_page).to be_nil
expect(TagName.find_by!(name: 'put_wiki_roundtrip_b').canonical).to eq(tag.tag_name)
versions = wiki_page.wiki_versions.order(:version_no).last(2)
@@ -1468,16 +1454,16 @@ RSpec.describe 'Tags API', type: :request do
end
it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do
old_owner = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_alias_old_owner'),
category: :general)
stolen_alias = create(:tag_name, :alias,
old_owner = Tag.create!(
tag_name: TagName.create!(name: 'put_alias_old_owner'),
category: :general
)
stolen_alias = TagName.create!(
name: 'put_stolen_alias',
tag: old_owner)
canonical: old_owner.tag_name
)
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.to include('put_stolen_alias')
expect(old_owner.tag_name.aliases.map(&:name)).to include('put_stolen_alias')
expect {
put "/tags/#{ tag.id }", params: {
@@ -1493,32 +1479,30 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
expect(stolen_alias.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.not_to include('put_stolen_alias')
expect(stolen_alias.reload.canonical).to eq(tag.tag_name)
expect(old_owner.reload.tag_name.aliases.map(&:name)).not_to include('put_stolen_alias')
old_owner_versions = old_owner.tag_versions.order(:version_no)
expect(old_owner_versions.first.event_type).to eq('create')
expect(version_alias_names(old_owner_versions.first, 'ja')).to include('put_stolen_alias')
expect(old_owner_versions.first.aliases.split).to include('put_stolen_alias')
expect(old_owner_versions.second.event_type).to eq('update')
expect(version_alias_names(old_owner_versions.second, 'ja'))
.not_to include('put_stolen_alias')
expect(old_owner_versions.second.aliases.split).not_to include('put_stolen_alias')
end
it '別 tag の alias 名を rename で奪へる' do
old_owner = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_alias_collision_owner'),
category: :general)
stolen_alias = create(:tag_name, :alias,
old_owner = Tag.create!(
tag_name: TagName.create!(name: 'put_alias_collision_owner'),
category: :general
)
stolen_alias = TagName.create!(
name: 'put_alias_collision_name',
tag: old_owner)
canonical: old_owner.tag_name
)
wiki_page =
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body: 'put collision wiki',
created_by_user: member_user,
message: 'init')
@@ -1531,33 +1515,29 @@ RSpec.describe 'Tags API', type: :request do
deprecated: '0',
}
expect(response).to have_http_status(:ok), response.body
expect(response).to have_http_status(:ok)
tag.reload
old_owner.reload
stolen_alias.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_alias_collision_name')
expect(stolen_alias).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: true)
expect(TagName.find_by!(language_code: 'ja', name: 'spec_tag'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.not_to include('put_alias_collision_name')
expect(tag.name).to eq('put_alias_collision_name')
expect(stolen_alias.canonical_id).to be_nil
expect(TagName.find_by!(name: 'spec_tag').canonical).to eq(tag.tag_name)
expect(old_owner.tag_name.aliases.map(&:name)).not_to include('put_alias_collision_name')
old_owner_versions = old_owner.tag_versions.order(:version_no)
expect(old_owner_versions.last.event_type).to eq('update')
expect(version_alias_names(old_owner_versions.last, 'ja'))
.not_to include('put_alias_collision_name')
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja'))
expect(old_owner_versions.last.aliases.split).not_to include('put_alias_collision_name')
expect(wiki_page.reload.tag_name).to eq(tag.tag_name)
end
it 'parent_tags に指定すると循環する tag は 422 にする' do
child = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_cycle_child'),
category: :general)
child = Tag.create!(
tag_name: TagName.create!(name: 'put_cycle_child'),
category: :general
)
TagImplication.create!(tag: child, parent_tag: tag)
@@ -1565,7 +1545,7 @@ RSpec.describe 'Tags API', type: :request do
name: 'spec_tag',
category: 'general',
aliases: 'unko',
parent_tags: primary_tag_name_for(child, 'ja').name,
parent_tags: child.name,
deprecated: '0',
}
@@ -1577,7 +1557,7 @@ RSpec.describe 'Tags API', type: :request do
it 'tag の name を変更すると対応する wiki version を作成する' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: primary_tag_name_for(tag, 'ja'),
tag_name: tag.tag_name,
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
+2 -2
ファイルの表示
@@ -396,7 +396,7 @@ RSpec.describe 'Theatres API', type: :request do
end
it 'finalizes skip when votes reach majority and stores voters and tag snapshots' do
tag = create(:tag, primary_name: 'skip-target')
tag = create(:tag, name: 'skip-target')
PostTag.create!(post: niconico_post, tag:)
TheatreSkipVote.create!(theatre:, post: niconico_post, user: member)
@@ -503,7 +503,7 @@ RSpec.describe 'Theatres API', type: :request do
end
it 'returns tag penalties and candidate weights for the current watchers' do
tag = create(:tag, primary_name: 'heavy-tag')
tag = create(:tag, name: 'heavy-tag')
PostTag.create!(post: second_niconico_post, tag:)
event = TheatreSkipEvent.create!(
theatre:,
+1 -1
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki conflict handling', type: :request do
it 'returns 409 when base_revision_id is stale' do
page =
Wiki::Commit.create_content!(
tag_name: create(:tag_name, name: 'wiki_conflict_request'),
tag_name: TagName.create!(name: 'wiki_conflict_request'),
body: 'first',
created_by_user: user,
message: 'init')
+1 -1
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki history integrity', type: :request do
def create_wiki_page title:, body: 'body', message: 'init', user: self.user
Wiki::Commit.create_content!(
tag_name: create(:tag_name, name: title),
tag_name: TagName.create!(name: title),
body:,
created_by_user: user,
message:)
+15 -15
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki API', type: :request do
let!(:user) { create_member_user! }
let!(:tn) { create(:tag_name, name: 'spec_wiki_title') }
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
let!(:page) do
Wiki::Commit.create_content!(
tag_name: tn,
@@ -19,10 +19,11 @@ RSpec.describe 'Wiki API', type: :request do
message: 'init')
end
let!(:tag) do
create(:tag,
primary_tag_name: tn,
Tag.create!(
tag_name: tn,
category: :general,
deprecated_at: Time.zone.local(2026, 6, 1))
deprecated_at: Time.zone.local(2026, 6, 1)
)
end
describe 'GET /wiki' do
@@ -207,7 +208,7 @@ RSpec.describe 'Wiki API', type: :request do
{ 'X-Transfer-Code' => user.inheritance_code }
end
let!(:test_tag_name) { create(:tag_name, name: 'TestPage') }
let!(:test_tag_name) { TagName.create!(name: 'TestPage') }
let!(:page) do
Wiki::Commit.create_content!(
@@ -274,8 +275,8 @@ RSpec.describe 'Wiki API', type: :request do
end
it 'wiki body だけを変更しても tag version は作成しない' do
linked_tag_name = create(:tag_name, name: 'wiki_body_only_tag')
linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general)
linked_tag_name = TagName.create!(name: 'wiki_body_only_tag')
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
TagVersionRecorder.record!(
tag: linked_tag,
@@ -358,13 +359,13 @@ RSpec.describe 'Wiki API', type: :request do
describe 'GET /wiki/search' do
before do
Wiki::Commit.create_content!(
tag_name: create(:tag_name, name: 'spec_wiki_title_2'),
tag_name: TagName.create!(name: 'spec_wiki_title_2'),
body: 'search body 2',
created_by_user: user,
message: 'init')
Wiki::Commit.create_content!(
tag_name: create(:tag_name, name: 'unrelated_title'),
tag_name: TagName.create!(name: 'unrelated_title'),
body: 'unrelated body',
created_by_user: user,
message: 'init')
@@ -433,7 +434,7 @@ RSpec.describe 'Wiki API', type: :request do
it 'returns empty array when page has no revisions and filtered by id' do
# 別ページを作って revision 無し
tn2 = create(:tag_name, name: 'spec_no_rev')
tn2 = TagName.create!(name: 'spec_no_rev')
# 異常データ: revision 無し WikiPage を直接作る
p2 = WikiPage.create!(
tag_name: tn2,
@@ -513,7 +514,7 @@ RSpec.describe 'Wiki API', type: :request do
describe 'Wiki::Commit.redirect!' do
it 'raises because redirect revisions are deprecated' do
target_tag_name = create(:tag_name, name: 'redirect_deprecated_target')
target_tag_name = TagName.create!(name: 'redirect_deprecated_target')
target =
Wiki::Commit.create_content!(
tag_name: target_tag_name,
@@ -534,8 +535,8 @@ RSpec.describe 'Wiki API', type: :request do
end
it 'wiki title を変更すると対応する tag の version を作成する' do
linked_tag_name = create(:tag_name, name: 'wiki_linked_tag_for_version')
linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general)
linked_tag_name = TagName.create!(name: 'wiki_linked_tag_for_version')
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
linked_page =
Wiki::Commit.create_content!(
@@ -563,8 +564,7 @@ RSpec.describe 'Wiki API', type: :request do
expect(response).to have_http_status(:ok)
linked_tag.reload
expect(primary_tag_name_for(linked_tag, 'ja').name)
.to eq('wiki_linked_tag_for_version_renamed')
expect(linked_tag.name).to eq('wiki_linked_tag_for_version_renamed')
versions = linked_tag.tag_versions.order(:version_no)
+1 -1
ファイルの表示
@@ -9,7 +9,7 @@ RSpec.describe 'Wiki title collision', type: :request do
def create_wiki_page title:, body:
Wiki::Commit.create_content!(
tag_name: create(:tag_name, name: title),
tag_name: TagName.create!(name: title),
body:,
created_by_user: user,
message: 'init')
+1 -1
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe MaterialSyncImporter do
let(:user) { create(:user, :member) }
let(:tag) { create(:tag, primary_name: 'sync_tag', category: :material) }
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'sync_tag'), category: :material) }
def tempfile_for body
Tempfile.new(['material-sync-importer', '.png']).tap do |file|
+1 -1
ファイルの表示
@@ -65,7 +65,7 @@ RSpec.describe NicoTagVersionRecorder do
end
it 'records sorted linked internal names and later link removal' do
tags = ['z_link', 'a_link'].map { |name| create(:tag, primary_name: name, category: :general) }
tags = ['z_link', 'a_link'].map { |name| Tag.create!(name:, category: :general) }
tags.each { |tag| NicoTagRelation.create!(nico_tag: external_tag, tag:) }
expect(record(:create).linked_tags).to eq('a_link z_link')
+1 -10
ファイルの表示
@@ -1,8 +1,6 @@
require 'rails_helper'
RSpec.describe PostBulkCreator do
include_context 'English locale'
it 'limits workers to two and keeps failures in their request slots' do
actor = instance_double(User, id: 123)
allow(User).to receive(:find).with(123) {
@@ -11,7 +9,6 @@ RSpec.describe PostBulkCreator do
mutex = Mutex.new
active = 0
maximum_active = 0
creators = []
allow(PostCreatePreflight).to receive(:new) do |attributes:, **|
preflight = instance_double(PostCreatePreflight)
@@ -31,7 +28,6 @@ RSpec.describe PostBulkCreator do
end
allow(PostCreator).to receive(:new) do |attributes:, **|
creator = instance_double(PostCreator)
mutex.synchronize { creators << creator }
if attributes[:title] == 'broken'
allow(creator).to receive(:create!).and_raise(StandardError, 'broken')
else
@@ -49,12 +45,7 @@ RSpec.describe PostBulkCreator do
results = described_class.new(
actor:,
posts:,
thumbnails: { }).run(locale).fetch(:results)
expect(creators.length).to eq(posts.length)
creators.each do |creator|
expect(creator).to have_received(:create!).with(locale).once
end
thumbnails: { }).run.fetch(:results)
expect(maximum_active).to eq(2)
expect(results.length).to eq(posts.length)
+9 -62
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe PostCreatePlan do
def create_tag! name, category
create(:tag, primary_name: name, category:)
Tag.create!(name:, category:)
end
before do
@@ -10,59 +10,6 @@ RSpec.describe PostCreatePlan do
create_tag!('ニジラー情報不詳', :meta)
end
context 'with an explicit locale' do
include_context 'English locale'
it 'resolves the matching language identity and its parents' do
english_name = create(:tag_name, name: 'shared_name',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name, category: :character)
japanese = create(:tag, primary_name: 'temporary_japanese', category: :general)
# The DB permits this identity; validation has its own contract spec.
primary_tag_name_for(japanese, 'ja').update_columns(name: 'shared_name')
english_parent_name = create(:tag_name, name: 'english_parent',
language_code: 'en', script_code: 'Latn')
english_parent = create(:tag, primary_tag_name: english_parent_name,
category: :material)
japanese_parent = create(:tag, primary_name: 'japanese_parent', category: :general)
TagImplication.create!(tag: english, parent_tag: english_parent)
TagImplication.create!(tag: japanese, parent_tag: japanese_parent)
plan = described_class.new(attributes: { tags: 'shared_name' }).build!(locale)
expect(plan[:direct_tag_specs]).to eq(
[{ name: 'shared_name', category: :character }])
expect(plan[:post_tag_specs]).to include(
{ name: primary_tag_name_for(english_parent, 'en').name, category: :material })
expect(plan[:post_tag_specs].pluck(:name))
.not_to include(primary_tag_name_for(japanese_parent, 'ja').name)
end
it 'resolves aliases within the requested language' do
japanese = create(:tag, primary_name: 'japanese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name, category: :character)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: japanese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
expect(japanese_alias.reload).to have_attributes(
tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
plan = described_class.new(attributes: { tags: 'shared_alias' }).build!(locale)
expect(plan[:direct_tag_specs]).to eq(
[{ name: english_name.name, category: :character }])
end
end
it 'plans direct and existing default tags without persisting records' do
counts = [TagName.count, Tag.count]
@@ -71,7 +18,7 @@ RSpec.describe PostCreatePlan do
url: 'https://example.com/post',
title: 'title',
tags: 'character:new_character',
parent_post_ids: '' }).build!(Locale.nipponese)
parent_post_ids: '' }).build!
expect(plan[:tags]).to eq('new_character')
expect(plan[:direct_tag_specs]).to eq(
@@ -82,9 +29,9 @@ RSpec.describe PostCreatePlan do
expect([TagName.count, Tag.count]).to eq(counts)
end
it 'resolves aliases and keeps tag sections separate from primary names' do
tag = create_tag!('虹夏', :character)
create(:tag_name, :alias, name: 'にじか', tag:)
it 'resolves aliases and keeps tag sections separate from canonical names' do
canonical = create_tag!('虹夏', :character)
TagName.create!(name: 'にじか', canonical: canonical.tag_name)
create_tag!('動画', :meta)
plan = described_class.new(
@@ -93,7 +40,7 @@ RSpec.describe PostCreatePlan do
title: 'video',
tags: '動画 にじか[0:10-0:20]',
duration: '1:00',
parent_post_ids: '' }).build!(Locale.nipponese)
parent_post_ids: '' }).build!
expect(plan[:tags].split).to include('動画', '虹夏[0:10-0:20]')
expect(plan[:display_tags]).to include(
@@ -108,7 +55,7 @@ RSpec.describe PostCreatePlan do
counts = [Tag.count, TagName.count, ExternalTag.count]
expect {
described_class.new(attributes: { tags: 'NiCo:reserved' }).build!(Locale.nipponese)
described_class.new(attributes: { tags: 'NiCo:reserved' }).build!
}.to raise_error(ActiveRecord::RecordInvalid) { |error|
expect(error.record.errors[:tags]).to be_present
}
@@ -126,7 +73,7 @@ RSpec.describe PostCreatePlan do
url: 'https://example.com/post',
title: 'title',
tags: long_name,
parent_post_ids: '' }).build!(Locale.nipponese)
parent_post_ids: '' }).build!
}.to raise_error(ActiveRecord::RecordInvalid) { |error|
expect(error.record.errors[:tags]).not_to be_empty
}
@@ -140,7 +87,7 @@ RSpec.describe PostCreatePlan do
title: 'title',
tags: 'ordinary_tag',
duration: 'invalid',
parent_post_ids: '' }).build!(Locale.nipponese)
parent_post_ids: '' }).build!
expect(plan[:duration]).to eq('invalid')
expect(plan[:video_ms]).to be_nil
+4 -30
ファイルの表示
@@ -14,6 +14,7 @@ RSpec.describe PostCreator do
end
before do
allow(Tag).to receive(:normalise_tags!).and_return({ tags: [], sections: {} })
allow(TagVersioning).to receive(:record_tag_snapshots!)
allow(Tag).to receive(:expand_parent_tags).and_return([])
allow(PostVersionRecorder).to receive(:record!)
@@ -33,7 +34,7 @@ RSpec.describe PostCreator do
url: 'https://example.com/post',
thumbnail: real_thumbnail_upload,
thumbnail_base: 'https://example.com/thumb.jpg',
tags: '' }).create!(Locale.nipponese)
tags: '' }).create!
expect(post.thumbnail).to be_attached
expect(post.thumbnail_base).to eq('https://example.com/thumb.jpg')
@@ -53,7 +54,7 @@ RSpec.describe PostCreator do
title: 'title',
url: 'https://example.com/post',
thumbnail_base: 'https://example.com/thumb.jpg',
tags: '' }).create!(Locale.nipponese)
tags: '' }).create!
expect(post.thumbnail_base).to eq('https://example.com/thumb.jpg')
expect(post.thumbnail).to be_attached
@@ -71,34 +72,7 @@ RSpec.describe PostCreator do
tags: '' })
post_count = Post.count
expect { creator.create!(Locale.nipponese) }
.to raise_error(Post::RemoteThumbnailFetchFailed)
expect { creator.create! }.to raise_error(Post::RemoteThumbnailFetchFailed)
expect(Post.count).to eq(post_count)
end
context 'with an explicit locale' do
include_context 'English locale'
[false, true].each do |planned|
it "passes locale to tag creation with planned attributes: #{ planned }" do
attributes = {
title: 'Locale propagation',
url: 'https://example.com/locale-post',
tags: 'character:locale_character' }
if planned
specs = [{ name: 'locale_character', category: :character }]
attributes.merge!(snapshot_tag_specs: specs, post_tag_specs: specs)
end
allow(Tag).to receive(:find_or_create_by_tag_name!).and_call_original
post = described_class.new(actor:, attributes:).create!(locale)
expect(Tag).to have_received(:find_or_create_by_tag_name!)
.with(locale, 'locale_character', category: :character).at_least(:once)
tag_name = TagName.find_by!(language_code: 'en', name: 'locale_character')
expect(tag_name).to have_attributes(script_code: 'Latn', primary_flg: true)
expect(post.tags).to include(tag_name.tag)
end
end
end
end
+1 -1
ファイルの表示
@@ -64,7 +64,7 @@ RSpec.describe PostImportPreviewer do
end
it 'applies metadata to automatic fields and recognises metadata tags' do
create(:tag, primary_name: 'known-tag', category: :general)
Tag.create!(name: 'known-tag', category: :general)
allow(PostMetadataFetcher).to receive(:fetch).and_return(
title: 'metadata title',
thumbnail_base: 'https://example.com/thumb.jpg',
-53
ファイルの表示
@@ -1,53 +0,0 @@
require 'rails_helper'
RSpec.describe TagVersionRecorder do
let(:user) { create_member_user! }
let(:tag) { create(:tag, primary_name: '日本語名') }
before do
Language.insert_all!([{ code: 'en', name: 'English', created_at: Time.current }])
Script.insert_all!([{ code: 'Latn', name: 'Latin', created_at: Time.current }])
Locale.insert_all!([{ code: 'en', language_code: 'en', script_code: 'Latn',
name: 'English', created_at: Time.current }])
create(:tag_name, :alias, tag:, name: '別名')
create(:tag_name, tag:, language_code: 'en', script_code: 'Latn', name: 'English name')
create(:tag_name, :alias, tag:, language_code: 'en', script_code: 'Latn',
name: 'English alias')
end
it 'snapshots every language primary name and alias deterministically' do
version = described_class.record!(tag:, event_type: :create, created_by_user: user)
expect(version.names).to eq([
{ 'language_code' => 'en', 'name' => 'English name', 'script_code' => 'Latn',
'primary_flg' => true, 'auto_generated' => false },
{ 'language_code' => 'en', 'name' => 'English alias', 'script_code' => 'Latn',
'primary_flg' => false, 'auto_generated' => false },
{ 'language_code' => 'ja', 'name' => '日本語名', 'script_code' => 'Jpan',
'primary_flg' => true, 'auto_generated' => false },
{ 'language_code' => 'ja', 'name' => '別名', 'script_code' => 'Jpan',
'primary_flg' => false, 'auto_generated' => false }
])
end
it 'creates a version for an English-only name change and preserves prior names' do
original = described_class.record!(tag:, event_type: :create, created_by_user: user)
TagName.find_by!(tag:, language_code: 'en', primary_flg: true).update!(name: 'Renamed')
updated = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(updated.version_no).to eq(2)
expect(original.reload.names).to include(
hash_including('language_code' => 'en', 'name' => 'English name'))
expect(updated.names).to include(hash_including('language_code' => 'en', 'name' => 'Renamed'))
end
it 'does not create a version for an unchanged multilingual snapshot' do
described_class.record!(tag:, event_type: :create, created_by_user: user)
unchanged = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(unchanged.version_no).to eq(1)
expect(tag.reload.tag_versions.count).to eq(1)
end
end
+5 -5
ファイルの表示
@@ -6,7 +6,7 @@ RSpec.describe Wiki::Commit do
def create_page title:, body: 'initial body'
described_class.create_content!(
tag_name: create(:tag_name, name: title),
tag_name: TagName.create!(name: title),
body:,
created_by_user: user,
message: 'init')
@@ -16,7 +16,7 @@ RSpec.describe Wiki::Commit do
it 'creates page, revision, and version with normalised body' do
expect {
described_class.create_content!(
tag_name: create(:tag_name, name: 'commit_integrity_create'),
tag_name: TagName.create!(name: 'commit_integrity_create'),
body: "a\r\nb\r\n\r\n",
created_by_user: user,
message: 'init')
@@ -36,7 +36,7 @@ RSpec.describe Wiki::Commit do
end
it 'rejects body that becomes blank after normalisation' do
tag_name = create(:tag_name, name: 'commit_integrity_blank')
tag_name = TagName.create!(name: 'commit_integrity_blank')
expect {
described_class.create_content!(
@@ -76,8 +76,8 @@ RSpec.describe Wiki::Commit do
end
it 'does not record tag_version on body-only wiki update' do
tag_name = create(:tag_name, name: 'commit_integrity_linked_tag')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
tag_name = TagName.create!(name: 'commit_integrity_linked_tag')
tag = Tag.create!(tag_name:, category: :general)
page =
described_class.create_content!(
+5 -5
ファイルの表示
@@ -4,7 +4,7 @@ RSpec.describe Wiki::Commit do
let(:user) { create_member_user! }
def create_page(title: 'commit_spec_page', body: 'initial body')
tag_name = create(:tag_name, name: title)
tag_name = TagName.create!(name: title)
Wiki::Commit.create_content!(
tag_name:,
@@ -80,8 +80,8 @@ RSpec.describe Wiki::Commit do
end
it 'does not record tag version when corresponding tag has no versions' do
tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions')
tag = Tag.create!(tag_name:, category: :general)
page =
described_class.create_content!(
@@ -107,8 +107,8 @@ RSpec.describe Wiki::Commit do
end
it 'does not record tag version when corresponding tag has no versions' do
tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions')
tag = Tag.create!(tag_name:, category: :general)
page =
described_class.create_content!(
+1 -1
ファイルの表示
@@ -5,7 +5,7 @@ RSpec.describe WikiVersionRecorder do
def create_page title:, body: 'body'
Wiki::Commit.create_content!(
tag_name: create(:tag_name, name: title),
tag_name: TagName.create!(name: title),
body:,
created_by_user: user,
message: 'init')
+2 -4
ファイルの表示
@@ -172,8 +172,7 @@ RSpec.describe Youtube::Sync do
Tag.video
Tag.no_deerjikist
deerjikist_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'テスト投稿者', category: :deerjikist)
deerjikist_tag = Tag.find_or_create_by_tag_name!('テスト投稿者', category: :deerjikist)
Deerjikist.create!(
platform: 'youtube',
code: 'UC_MAPPED',
@@ -227,8 +226,7 @@ RSpec.describe Youtube::Sync do
)
PostTag.create!(post:, tag: Tag.no_deerjikist)
deerjikist_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, '後から判明した投稿者', category: :deerjikist)
deerjikist_tag = Tag.find_or_create_by_tag_name!('後から判明した投稿者', category: :deerjikist)
Deerjikist.create!(
platform: 'youtube',
code: 'UC_MAPPED_LATER',
-13
ファイルの表示
@@ -1,13 +0,0 @@
RSpec.shared_context 'English locale' do
let!(:locale) do
Language.find_or_create_by!(code: 'en') { _1.name = 'English' }
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
unless Locale.exists?(code: 'en')
# Reference data only: do not generate names for unrelated existing tags.
Locale.insert_all!([
{ code: 'en', language_code: 'en', script_code: 'Latn',
name: 'English', created_at: Time.current }])
end
Locale.find('en')
end
end
-4
ファイルの表示
@@ -1,8 +1,4 @@
module TestRecords
def primary_tag_name_for(tag, language_code)
TagName.find_by!(tag_id: tag.id, language_code:, primary_flg: true)
end
def create_member_user!
User.create!(name: 'spec user',
inheritance_code: SecureRandom.hex(16),
+6 -15
ファイルの表示
@@ -8,7 +8,7 @@ RSpec.describe 'nico:sync' do
end
def create_tag!(name, category:)
Tag.find_or_create_by_tag_name!(Locale.nipponese, name, category:)
Tag.find_or_create_by_tag_name!(name, category:)
end
def link_nico_to_tag!(nico_tag, tag)
@@ -64,8 +64,7 @@ RSpec.describe 'nico:sync' do
run_rake_task('nico:sync')
post.reload
active_tag_names = TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
active_tag_names = post.tags.joins(:tag_name).pluck('tag_names.name')
expect(active_tag_names).to include('spec_kept')
expect(post.external_tags).to contain_exactly(nico)
@@ -435,9 +434,7 @@ RSpec.describe 'nico:sync' do
external = post.external_tags.sole
expect(external.name).to eq('raw tag[]')
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
expect(post.tags.map(&:name)).not_to include('bot操作')
expect(post.post_versions.order(:version_no).last.tags_json)
.to include('external_tag_id' => external.id)
@@ -449,9 +446,7 @@ RSpec.describe 'nico:sync' do
run_nico_sync_with_tags!([])
}.to change(PostVersion, :count).by(1)
expect(post.reload.external_tags).to be_empty
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
expect(post.tags.map(&:name)).not_to include('bot操作')
end
it 'サニタイズ後の既存外部タグを再利用し、その連携タグを記載する' do
@@ -568,9 +563,7 @@ RSpec.describe 'nico:sync' do
name: 'AAA')
expect(ExternalTag.exists?(platform: :nico, name: 'AAA?')).to be(false)
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
expect(post.tags.map(&:name)).not_to include('bot操作')
expect(post.post_versions.order(:version_no).last.tags_json)
.to include('external_tag_id' => external.id)
@@ -584,9 +577,7 @@ RSpec.describe 'nico:sync' do
}.to change(PostVersion, :count).by(1)
expect(post.reload.external_tags).to be_empty
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
expect(post.tags.map(&:name)).not_to include('bot操作')
end
it 'nico: prefix を含む従来の規則で外部タグ名をサニタイズする' do
+4 -4
ファイルの表示
@@ -6,10 +6,10 @@ RSpec.describe 'post_similarity:calc' do
it 'calculates similarities from active tags only' do
# 必要最低限のデータ
t1 = create(:tag, primary_name: 't1')
t2 = create(:tag, primary_name: 't2')
t3 = create(:tag, primary_name: 't3')
deprecated_tag = create(:tag, primary_name: 'deprecated', deprecated_at: Time.current)
t1 = Tag.create!(name: "t1")
t2 = Tag.create!(name: "t2")
t3 = Tag.create!(name: "t3")
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current)
p1 = Post.create!(url: "https://example.com/1")
p2 = Post.create!(url: "https://example.com/2")
+4 -4
ファイルの表示
@@ -6,10 +6,10 @@ RSpec.describe 'tag_similarity:calc' do
it 'calculates similarities for active tags only' do
# 必要最低限のデータ
t1 = create(:tag, primary_name: 't1')
t2 = create(:tag, primary_name: 't2')
t3 = create(:tag, primary_name: 't3')
deprecated_tag = create(:tag, primary_name: 'deprecated', deprecated_at: Time.current)
t1 = Tag.create!(name: "t1")
t2 = Tag.create!(name: "t2")
t3 = Tag.create!(name: "t3")
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current)
p1 = Post.create!(url: "https://example.com/1")
p2 = Post.create!(url: "https://example.com/2")