Reviewed-on: #421 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #421 でマージされました。
このコミットが含まれているのは:
@@ -92,4 +92,6 @@ class ApplicationController < ActionController::API
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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?
|
||||
@@ -7,10 +9,10 @@ class DeerjikistsController < ApplicationController
|
||||
|
||||
deerjikist = Deerjikist
|
||||
.joins(:tag)
|
||||
.includes(tag: :tag_name)
|
||||
.includes(tag: :tag_names)
|
||||
.find_by(platform:, code:)
|
||||
if deerjikist
|
||||
render json: DeerjikistRepr.base(deerjikist)
|
||||
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
@@ -20,6 +22,7 @@ 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
|
||||
@@ -32,7 +35,7 @@ class DeerjikistsController < ApplicationController
|
||||
d.save!
|
||||
end
|
||||
|
||||
render json: DeerjikistRepr.base(deerjikist)
|
||||
render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code)
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -2,7 +2,7 @@ class GekanatorPostsController < ApplicationController
|
||||
def index
|
||||
posts =
|
||||
Post
|
||||
.preload(:post_similarities, tags: :tag_name)
|
||||
.preload(:post_similarities, tags: :tag_names)
|
||||
.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,
|
||||
name: tag.name('ja'),
|
||||
category: tag.category
|
||||
}
|
||||
end
|
||||
|
||||
@@ -126,9 +126,9 @@ class GekanatorQuestionsController < ApplicationController
|
||||
end
|
||||
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.joins(:tag_names)
|
||||
.where(category: categories.uniq)
|
||||
.where(tag_names: { name: names.uniq })
|
||||
.where(tag_names: { language_code: 'ja', primary_flg: true, name: names.uniq })
|
||||
.where.not(deprecated_at: nil)
|
||||
.pluck('tags.category', 'tag_names.name')
|
||||
.each_with_object({ }) do |(category, name), h|
|
||||
|
||||
@@ -7,6 +7,8 @@ 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
|
||||
|
||||
@@ -20,7 +22,7 @@ class MaterialsController < ApplicationController
|
||||
q = Material.includes(:material_export_items,
|
||||
thumbnail_attachment: :blob,
|
||||
file_attachment: :blob,
|
||||
tag: :tag_name)
|
||||
tag: :tag_names)
|
||||
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)
|
||||
@@ -42,7 +44,10 @@ class MaterialsController < ApplicationController
|
||||
.offset(offset)
|
||||
.to_a
|
||||
|
||||
response = { materials: MaterialRepr.list_many(materials, host: request.base_url),
|
||||
response = { materials: MaterialRepr.list_many(
|
||||
materials,
|
||||
host: request.base_url,
|
||||
language_code: locale.language_code),
|
||||
count: }
|
||||
if filters[:tag_id]
|
||||
response[:tag_scope] = material_index_tag_scope(filters, tag_graph)
|
||||
@@ -55,6 +60,8 @@ class MaterialsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
locale = resolve_locale!
|
||||
|
||||
material =
|
||||
Material
|
||||
.includes(:tag, :material_export_items)
|
||||
@@ -63,15 +70,17 @@ class MaterialsController < ApplicationController
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless material
|
||||
|
||||
wiki_page_body = material.tag&.tag_name&.wiki_page&.current_revision&.body
|
||||
wiki_page_body = material.tag&.tag_name('ja')&.wiki_page&.current_revision&.body
|
||||
|
||||
render json: MaterialRepr.base(material, host: request.base_url).merge(wiki_page_body:)
|
||||
render json: MaterialRepr.base(material, host: request.base_url,
|
||||
language_code: locale.language_code).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)
|
||||
@@ -89,13 +98,14 @@ class MaterialsController < ApplicationController
|
||||
|
||||
begin
|
||||
Material.transaction do
|
||||
tag = resolve_material_tag!(tag_name_raw)
|
||||
tag = resolve_material_tag!(locale, 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)
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
upsert_export_paths!(material)
|
||||
MaterialVersionRecorder.record!(material:, event_type: :create,
|
||||
created_by_user: current_user)
|
||||
@@ -107,7 +117,8 @@ class MaterialsController < ApplicationController
|
||||
|
||||
if material
|
||||
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
||||
render json: MaterialRepr.base(material, host: request.base_url), status: :created
|
||||
render json: MaterialRepr.base(material, host: request.base_url,
|
||||
language_code: locale.language_code), status: :created
|
||||
else
|
||||
render_validation_error material
|
||||
end
|
||||
@@ -132,12 +143,14 @@ 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!(tag_name_raw)
|
||||
tag = resolve_material_tag!(locale, tag_name_raw)
|
||||
material.assign_attributes(tag:, url:, updated_by_user: current_user)
|
||||
if uploaded_blob
|
||||
material.file.attach(uploaded_blob)
|
||||
@@ -145,7 +158,8 @@ class MaterialsController < ApplicationController
|
||||
material.file.detach
|
||||
end
|
||||
material.save!
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
upsert_export_paths!(material)
|
||||
MaterialVersionRecorder.record!(material:, event_type: :update,
|
||||
created_by_user: current_user)
|
||||
@@ -157,7 +171,8 @@ class MaterialsController < ApplicationController
|
||||
|
||||
log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material))
|
||||
|
||||
render json: MaterialRepr.base(material, host: request.base_url)
|
||||
render json: MaterialRepr.base(material, host: request.base_url,
|
||||
language_code: locale.language_code)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -236,10 +251,8 @@ class MaterialsController < ApplicationController
|
||||
nil
|
||||
end
|
||||
|
||||
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)
|
||||
def resolve_material_tag! locale, tag_name_raw
|
||||
Tag.find_or_create_by_tag_name!(locale, tag_name_raw, category: :material)
|
||||
end
|
||||
|
||||
def material_index_needs_tag_name? filters
|
||||
@@ -253,7 +266,12 @@ class MaterialsController < ApplicationController
|
||||
end
|
||||
|
||||
def material_index_join_tag_name q
|
||||
q.left_joins(tag: :tag_name)
|
||||
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
|
||||
end
|
||||
|
||||
def material_index_join_file_blob q
|
||||
@@ -269,9 +287,11 @@ class MaterialsController < ApplicationController
|
||||
|
||||
def apply_material_query q, term
|
||||
like = "%#{ ActiveRecord::Base.sanitize_sql_like(term) }%"
|
||||
q.where('tag_names.name LIKE :q OR materials.url LIKE :q OR ' \
|
||||
'material_file_blobs.filename LIKE :q',
|
||||
q: like)
|
||||
q.where(<<~SQL.squish, q: like)
|
||||
tag_names.name LIKE :q
|
||||
OR materials.url LIKE :q
|
||||
OR material_file_blobs.filename LIKE :q
|
||||
SQL
|
||||
end
|
||||
|
||||
def apply_material_media_kind q, media_kind
|
||||
@@ -323,8 +343,9 @@ class MaterialsController < ApplicationController
|
||||
end
|
||||
tags_by_id =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.where(id: scope_tag_ids)
|
||||
.joins(:tag_names)
|
||||
.where(id: scope_tag_ids,
|
||||
tag_names: { language_code: 'ja', primary_flg: true })
|
||||
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
||||
.each_with_object({ }) do |(id, name, category, deprecated_at), hash|
|
||||
hash[id] = { id:, name:, category:, deprecated: deprecated_at.present? }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
class NicoTagsController < ApplicationController
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
|
||||
name = params[:name].presence
|
||||
linked_tag = params[:linked_tag].presence
|
||||
link_status = params[:link_status].presence
|
||||
@@ -24,7 +26,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_name: :wiki_page })
|
||||
.includes(linked_tags: { tag_names: :wiki_page })
|
||||
if name
|
||||
q = q.where(('external_tags.name LIKE ? ' +
|
||||
"OR CONCAT(external_tags.platform, ':', external_tags.name) LIKE ?"),
|
||||
@@ -34,7 +36,8 @@ class NicoTagsController < ApplicationController
|
||||
if linked_tag
|
||||
linked_tag_ids =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: 'ja', primary_flg: true })
|
||||
.where('tag_names.name LIKE ?', "%#{ linked_tag }%")
|
||||
.pluck(:id)
|
||||
linked_nico_tag_ids = NicoTagRelation.where(tag_id: linked_tag_ids).pluck(:nico_tag_id)
|
||||
@@ -67,7 +70,9 @@ 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) })
|
||||
linked_tags: tag.linked_tags.map { |lt|
|
||||
TagRepr.base(lt, language_code: locale.language_code)
|
||||
})
|
||||
}, count: }
|
||||
end
|
||||
|
||||
@@ -81,6 +86,8 @@ 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)
|
||||
@@ -89,10 +96,14 @@ class NicoTagsController < ApplicationController
|
||||
linked_tags = nil
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
linked_tags = Tag.normalise_tags!(locale, linked_tag_names,
|
||||
with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
|
||||
TagVersioning.record_tag_snapshots!(linked_tags, created_by_user: current_user)
|
||||
TagVersioning.record_tag_snapshots!(
|
||||
linked_tags,
|
||||
created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
|
||||
tag.linked_tags = linked_tags
|
||||
tag.save!
|
||||
@@ -102,7 +113,9 @@ class NicoTagsController < ApplicationController
|
||||
created_by_user: current_user)
|
||||
end
|
||||
|
||||
render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok
|
||||
render json: tag.linked_tags.map { |t|
|
||||
TagRepr.base(t, language_code: locale.language_code)
|
||||
}, status: :ok
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
render_validation_error fields: { tags: ['ニコニコ・タグ同士は連携できません.'] }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
|
||||
@@ -7,6 +7,8 @@ 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
|
||||
@@ -46,12 +48,12 @@ class PostsController < ApplicationController
|
||||
'COALESCE(pt_max.max_updated_at, posts.updated_at))'
|
||||
|
||||
q =
|
||||
filtered_posts
|
||||
filtered_posts(locale)
|
||||
.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_name: :wiki_page }] }])
|
||||
{ tag_names: :wiki_page }] }])
|
||||
.with_attached_thumbnail
|
||||
|
||||
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
||||
@@ -90,7 +92,8 @@ class PostsController < ApplicationController
|
||||
q = q.except(:select, :order)
|
||||
|
||||
render json: { posts: posts.map { |post|
|
||||
PostRepr.base(post).merge(updated_at: post.updated_at_all).tap do |json|
|
||||
PostRepr.base(post, language_code: locale.language_code)
|
||||
.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)
|
||||
@@ -102,17 +105,19 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def random
|
||||
locale = resolve_locale!
|
||||
|
||||
post =
|
||||
filtered_posts
|
||||
filtered_posts(locale)
|
||||
.preload(:uploaded_user, :parents, :children,
|
||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||
{ tag_name: :wiki_page }] }])
|
||||
{ tag_names: :wiki_page }] }])
|
||||
.with_attached_thumbnail
|
||||
.order('RAND()')
|
||||
.first
|
||||
return head :not_found unless post
|
||||
|
||||
render json: PostRepr.base(post, current_user)
|
||||
render json: PostRepr.base(post, current_user, language_code: locale.language_code)
|
||||
end
|
||||
|
||||
def metadata
|
||||
@@ -187,11 +192,13 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
locale = resolve_locale!
|
||||
|
||||
post =
|
||||
Post
|
||||
.includes(:uploaded_user, :parents, :children,
|
||||
post_tags: [:sections, { tag: [:deerjikists, :materials,
|
||||
{ tag_name: :wiki_page }] }])
|
||||
{ tag_names: :wiki_page }] }])
|
||||
.with_attached_thumbnail
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless post
|
||||
@@ -205,18 +212,21 @@ class PostsController < ApplicationController
|
||||
parent_posts:,
|
||||
child_posts:,
|
||||
sibling_posts:,
|
||||
related:)
|
||||
.merge(tags: build_tag_tree_for(post))
|
||||
related:,
|
||||
language_code: locale.language_code)
|
||||
.merge(tags: build_tag_tree_for(post, locale.language_code))
|
||||
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
|
||||
host: request.base_url).run(locale)
|
||||
return render json: dry_run_json(preflight) if bool?(:dry)
|
||||
if preflight[:existing_post_id].present?
|
||||
post = Post.new(url: preflight[:url])
|
||||
@@ -242,10 +252,10 @@ class PostsController < ApplicationController
|
||||
:post_tag_specs,
|
||||
:tag_sections,
|
||||
:normalised_parent_post_ids).symbolize_keys).merge(
|
||||
thumbnail: params[:thumbnail])).create!
|
||||
thumbnail: params[:thumbnail])).create!(locale)
|
||||
|
||||
post.reload
|
||||
render json: PostRepr.base(post), status: :created
|
||||
render json: PostRepr.base(post, language_code: locale.language_code), status: :created
|
||||
rescue PostCreatePreflight::ValidationFailed => e
|
||||
render_validation_error fields: e.fields, base: e.base_errors
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
@@ -271,13 +281,18 @@ 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
|
||||
host: request.base_url).run(locale)
|
||||
|
||||
render json: result
|
||||
rescue JSON::ParserError
|
||||
render_bad_request 'posts manifest の JSON が不正です.'
|
||||
@@ -310,6 +325,7 @@ 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]
|
||||
@@ -329,9 +345,10 @@ 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)
|
||||
current_snapshot = post_snapshot_from_record(post, locale)
|
||||
end
|
||||
incoming_snapshot = post_incoming_snapshot(title:,
|
||||
incoming_snapshot = post_incoming_snapshot(locale:,
|
||||
title:,
|
||||
original_created_from:,
|
||||
original_created_before:,
|
||||
tag_names:,
|
||||
@@ -360,14 +377,14 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
apply_post_snapshot!(post, snapshot_to_apply)
|
||||
apply_post_snapshot!(locale, post, snapshot_to_apply)
|
||||
end
|
||||
|
||||
return render json: conflict_json, status: :conflict if conflict_json
|
||||
|
||||
post.reload
|
||||
json = PostRepr.base(post, current_user)
|
||||
json['tags'] = build_tag_tree_for(post)
|
||||
json = PostRepr.base(post, current_user, language_code: locale.language_code)
|
||||
json['tags'] = build_tag_tree_for(post, locale.language_code)
|
||||
render json:, status: :ok
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
|
||||
@@ -385,19 +402,19 @@ class PostsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def filtered_posts
|
||||
def filtered_posts locale
|
||||
tag_names = params[:tags].to_s.split
|
||||
match_type = params[:match]
|
||||
if tag_names.present?
|
||||
filter_posts_by_tags(tag_names, match_type)
|
||||
filter_posts_by_tags(locale, tag_names, match_type)
|
||||
else
|
||||
Post.all
|
||||
end
|
||||
end
|
||||
|
||||
def filter_posts_by_tags tag_names, match_type
|
||||
def filter_posts_by_tags locale, tag_names, match_type
|
||||
literals = tag_names.map do |raw_name|
|
||||
{ name: TagName.canonicalise(raw_name.sub(/\Anot:/i, '')).first,
|
||||
{ name: TagName.canonicalise(locale, raw_name.sub(/\Anot:/i, '')).first,
|
||||
negative: raw_name.downcase.start_with?('not:') }
|
||||
end
|
||||
|
||||
@@ -405,11 +422,11 @@ class PostsController < ApplicationController
|
||||
|
||||
if match_type == 'any'
|
||||
literals.reduce(Post.none) do |posts, literal|
|
||||
posts.or(tag_literal_relation(literal[:name], negative: literal[:negative]))
|
||||
posts.or(tag_literal_relation(locale, literal[:name], negative: literal[:negative]))
|
||||
end
|
||||
else
|
||||
literals.reduce(Post.all) do |posts, literal|
|
||||
ids = tagged_post_ids_for(literal[:name])
|
||||
ids = tagged_post_ids_for(locale, literal[:name])
|
||||
if literal[:negative]
|
||||
posts.where.not(id: ids)
|
||||
else
|
||||
@@ -419,8 +436,8 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def tag_literal_relation name, negative:
|
||||
ids = tagged_post_ids_for(name)
|
||||
def tag_literal_relation locale, name, negative:
|
||||
ids = tagged_post_ids_for(locale, name)
|
||||
if negative
|
||||
Post.where.not(id: ids)
|
||||
else
|
||||
@@ -428,11 +445,11 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def tagged_post_ids_for(name)
|
||||
def tagged_post_ids_for locale, name
|
||||
posts_by_internal_tags =
|
||||
Post
|
||||
.joins(tags: :tag_name)
|
||||
.where(tag_names: { name: })
|
||||
.joins(tags: :tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, name: })
|
||||
.select(:id)
|
||||
|
||||
posts_by_external_tags =
|
||||
@@ -474,7 +491,7 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def build_tag_tree_for post
|
||||
def build_tag_tree_for post, language_code = 'ja'
|
||||
post_tags = post.post_tags.reject { |post_tag| post_tag.tag.deprecated? }
|
||||
tags = post_tags.map(&:tag)
|
||||
tag_ids = tags.map(&:id)
|
||||
@@ -504,7 +521,7 @@ class PostsController < ApplicationController
|
||||
sections = sections_by_tag_id.fetch(tag_id, [])
|
||||
|
||||
if path.include?(tag_id)
|
||||
return TagRepr.inline(tag).merge(children: [], sections:)
|
||||
return TagRepr.inline(tag, language_code:).merge(children: [], sections:)
|
||||
end
|
||||
|
||||
if memo.key?(tag_id)
|
||||
@@ -516,7 +533,7 @@ class PostsController < ApplicationController
|
||||
|
||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
||||
|
||||
memo[tag_id] = TagRepr.inline(tag).merge(children:, sections:)
|
||||
memo[tag_id] = TagRepr.inline(tag, language_code:).merge(children:, sections:)
|
||||
end
|
||||
|
||||
internal_tags = root_ids.filter_map { |id| build_node.call(id, []) }
|
||||
@@ -672,40 +689,46 @@ 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
|
||||
def post_snapshot_from_record post, locale
|
||||
{ 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),
|
||||
tag_names: editable_tag_names_from_post(post, locale.language_code),
|
||||
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
|
||||
end
|
||||
|
||||
def editable_tag_names_from_post post
|
||||
def editable_tag_names_from_post post, language_code
|
||||
post
|
||||
.post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.joins(tag: :tag_names)
|
||||
.merge(Tag.where(deprecated_at: nil))
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.order('tag_names.name')
|
||||
.map do |post_tag|
|
||||
name = post_tag.tag.tag_name.name
|
||||
name = post_tag.tag.name(language_code)
|
||||
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||
|
||||
next name if sections.empty?
|
||||
|
||||
"#{ name }#{ sections.map { Post.section_literal(_1) }.join }"
|
||||
end
|
||||
end.compact.sort
|
||||
end
|
||||
|
||||
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
||||
def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:,
|
||||
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
||||
validate_original_created_values!(original_created_from, original_created_before)
|
||||
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||
with_sections: true) => { tags:, sections: }
|
||||
|
||||
Tag.normalise_tags!(locale, tag_names,
|
||||
with_tagme: false,
|
||||
with_no_deerjikist: false,
|
||||
deny_deprecated: true,
|
||||
with_sections: true) => { tags:, sections: }
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:)
|
||||
@@ -716,7 +739,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 }#{ sections[tag.id].to_a.map { section_literal(_1) }.join }"
|
||||
"#{ tag.name(locale.language_code) }#{ sections[tag.id].to_a.map { section_literal(_1) }.join }"
|
||||
}.sort,
|
||||
parent_post_ids: parent_post_ids.sort }
|
||||
end
|
||||
@@ -840,7 +863,7 @@ class PostsController < ApplicationController
|
||||
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
|
||||
end
|
||||
|
||||
def apply_post_snapshot! post, snapshot
|
||||
def apply_post_snapshot! locale, post, snapshot
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
||||
|
||||
post.update!(title: snapshot[:title],
|
||||
@@ -848,11 +871,12 @@ class PostsController < ApplicationController
|
||||
original_created_from: snapshot[:original_created_from],
|
||||
original_created_before: snapshot[:original_created_before])
|
||||
|
||||
Tag.normalise_tags!(snapshot[:tag_names],
|
||||
Tag.normalise_tags!(locale, snapshot[:tag_names],
|
||||
with_tagme: false,
|
||||
deny_deprecated: true,
|
||||
with_sections: true) => { tags:, sections: }
|
||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user)
|
||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -18,7 +19,8 @@ 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.parent_tag_ids AS prev_parent_tag_ids')
|
||||
'prev.aliases AS prev_aliases', 'prev.names AS prev_names',
|
||||
'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
|
||||
@@ -27,18 +29,20 @@ class TagVersionsController < ApplicationController
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
render json: { versions: serialise_versions(versions), count: }
|
||||
render json: { versions: serialise_versions(versions, locale.language_code), count: }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialise_versions rows
|
||||
def serialise_versions rows, language_code
|
||||
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|
|
||||
cur_aliases = split_values(row.aliases)
|
||||
prev_aliases = split_values(row.attributes['prev_aliases'])
|
||||
current_names = names_for_locale(row.names, language_code)
|
||||
previous_names = names_for_locale(row.attributes['prev_names'], language_code)
|
||||
cur_aliases = current_names.fetch('aliases', []).map { _1.fetch('name') }
|
||||
prev_aliases = previous_names.fetch('aliases', []).map { _1.fetch('name') }
|
||||
|
||||
cur_parent_tag_ids = split_parent_tag_ids(row.parent_tag_ids)
|
||||
prev_parent_tag_ids = split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])
|
||||
@@ -47,21 +51,22 @@ class TagVersionsController < ApplicationController
|
||||
|
||||
tags_by_id =
|
||||
Tag
|
||||
.includes(:tag_name, :materials, { tag_name: :wiki_page })
|
||||
.includes(:tag_names, :materials, tag_names: :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]]),
|
||||
{ tag: TagRepr.base(tags_by_id[h[:tag_id]], language_code:),
|
||||
type: h[:type] }
|
||||
end
|
||||
|
||||
{ tag_id: row.tag_id,
|
||||
version_no: row.version_no,
|
||||
event_type: row.event_type,
|
||||
name: { current: row.name, prev: row.attributes['prev_name'] },
|
||||
names: row.names,
|
||||
name: { current: current_names['name'], prev: previous_names['name'] },
|
||||
category: { current: row.category, prev: row.attributes['prev_category'] },
|
||||
deprecated_at: { current: row.deprecated_at&.iso8601,
|
||||
prev: row.attributes['prev_deprecated_at']&.iso8601 },
|
||||
@@ -91,5 +96,12 @@ 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) : names
|
||||
values&.fetch(language_code, {}) || {}
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
def split_parent_tag_ids(values) = split_values(values).map(&:to_i)
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@ require 'set'
|
||||
|
||||
class TagsController < ApplicationController
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
|
||||
post_id = params[:post]
|
||||
|
||||
name = params[:name].presence
|
||||
@@ -36,9 +38,14 @@ class TagsController < ApplicationController
|
||||
|
||||
tags =
|
||||
if post_id.present?
|
||||
Tag.joins(:posts, :tag_name).where(posts: { id: post_id })
|
||||
Tag
|
||||
.joins(:posts, :tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
posts: { id: post_id })
|
||||
else
|
||||
Tag.joins(:tag_name)
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true })
|
||||
end
|
||||
|
||||
external_tags =
|
||||
@@ -167,9 +174,10 @@ class TagsController < ApplicationController
|
||||
|
||||
tags_by_id =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.where(id: tag_ids)
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
id: tag_ids)
|
||||
.index_by(&:id)
|
||||
external_tags_by_id =
|
||||
ExternalTag
|
||||
@@ -178,7 +186,7 @@ class TagsController < ApplicationController
|
||||
|
||||
render json: { tags: rows.map { |row|
|
||||
if row['source'] == 'tag'
|
||||
TagRepr.base(tags_by_id.fetch(row['id']))
|
||||
TagRepr.base(tags_by_id.fetch(row['id']), language_code: locale.language_code)
|
||||
else
|
||||
external_tag_json(external_tags_by_id.fetch(row['id']))
|
||||
end
|
||||
@@ -190,7 +198,8 @@ class TagsController < ApplicationController
|
||||
parent_tag_id = nil if parent_tag_id <= 0
|
||||
material_filter = material_filter_param(default: 'any')
|
||||
|
||||
graph = build_with_depth_graph(material_filter)
|
||||
locale = resolve_locale!
|
||||
graph = build_with_depth_graph(material_filter, locale.language_code)
|
||||
|
||||
tag_ids =
|
||||
if parent_tag_id
|
||||
@@ -205,6 +214,8 @@ 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) }%"
|
||||
|
||||
@@ -213,35 +224,36 @@ class TagsController < ApplicationController
|
||||
|
||||
alias_rows =
|
||||
TagName
|
||||
.where(language_code: locale.language_code, primary_flg: false)
|
||||
.where('name LIKE ?', prefix)
|
||||
.where.not(canonical_id: nil)
|
||||
.pluck(:canonical_id, :name)
|
||||
.pluck(:tag_id, :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
|
||||
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
|
||||
end
|
||||
|
||||
base =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.where(deprecated_at: nil)
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
deprecated_at: nil)
|
||||
base = 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(tag_name_id: canonical_ids.uniq))
|
||||
internal_tags = canonical_hit.or(base.where(id: tag_ids.uniq))
|
||||
|
||||
internal_rows =
|
||||
internal_tags
|
||||
.order(Arel.sql('tags.post_count DESC, tag_names.name'))
|
||||
.limit(20)
|
||||
.map { |tag|
|
||||
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
|
||||
TagRepr.base(tag, language_code: locale.language_code)
|
||||
.merge(matched_alias: matched_alias_by_tag_id[tag.id])
|
||||
}
|
||||
|
||||
return render json: internal_rows unless with_nico
|
||||
@@ -257,20 +269,23 @@ class TagsController < ApplicationController
|
||||
.map { external_tag_json(_1) }
|
||||
|
||||
rows =
|
||||
(internal_rows + external_rows)
|
||||
.sort_by { |row| [-row['post_count'], row['name']] }
|
||||
(internal_rows + external_rows)
|
||||
.sort_by { |row| [-row['post_count'], row['name'] || row[:name]] }
|
||||
.first(20)
|
||||
|
||||
render json: rows
|
||||
end
|
||||
|
||||
def show
|
||||
locale = resolve_locale!
|
||||
|
||||
tag =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.find_by(id: params[:id])
|
||||
return render json: TagRepr.base(tag) if tag
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.find_by(id: params[:id],
|
||||
tag_names: { language_code: locale.language_code, primary_flg: true })
|
||||
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
|
||||
|
||||
external_tag = ExternalTag.find_by(id: params[:id])
|
||||
return render json: ExternalTagRepr.base(external_tag) if external_tag
|
||||
@@ -282,12 +297,14 @@ class TagsController < ApplicationController
|
||||
name = params[:name].to_s.strip
|
||||
return render_bad_request('name は必須です.') if name.blank?
|
||||
|
||||
locale = resolve_locale!
|
||||
|
||||
tag =
|
||||
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
|
||||
.joins(:tag_names)
|
||||
.includes(:tag_names, :materials, tag_names: :wiki_page)
|
||||
.find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true })
|
||||
return render json: TagRepr.base(tag, language_code: locale.language_code) if tag
|
||||
|
||||
platform, external_name = name.split(':', 2)
|
||||
return head :not_found unless external_name
|
||||
@@ -299,35 +316,52 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
def deerjikists
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, tag_name: :wiki_page)
|
||||
.find_by(id: params[:id])
|
||||
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 })
|
||||
return head :not_found unless tag
|
||||
|
||||
render json: { tag: TagRepr.base(tag),
|
||||
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
|
||||
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
|
||||
deerjikists: DeerjikistRepr.many(
|
||||
tag.deerjikists,
|
||||
language_code: locale.language_code) }
|
||||
end
|
||||
|
||||
def deerjikists_by_name
|
||||
name = params[:name].to_s.strip
|
||||
return render_bad_request('name は必須です.') if name.blank?
|
||||
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, tag_name: :wiki_page)
|
||||
.find_by(tag_names: { name: })
|
||||
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 })
|
||||
return head :not_found unless tag
|
||||
|
||||
render json: { tag: TagRepr.base(tag),
|
||||
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
|
||||
render json: { tag: TagRepr.base(tag, language_code: locale.language_code),
|
||||
deerjikists: DeerjikistRepr.many(
|
||||
tag.deerjikists,
|
||||
language_code: locale.language_code) }
|
||||
end
|
||||
|
||||
def update_deerjikists
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
tag = Tag.joins(:tag_name)
|
||||
.includes(:tag_name, tag_name: :wiki_page)
|
||||
.find_by(id: params[:id])
|
||||
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 })
|
||||
return head :not_found unless tag
|
||||
|
||||
rows = normalise_deerjikist_rows(tag)
|
||||
@@ -346,7 +380,7 @@ class TagsController < ApplicationController
|
||||
row_indexes_by_key.key?([deerjikist.platform, deerjikist.code])
|
||||
}
|
||||
|
||||
render_deerjikist_conflicts(requested_deerjikists, row_indexes_by_key, tag)
|
||||
render_deerjikist_conflicts(locale, requested_deerjikists, row_indexes_by_key, tag)
|
||||
raise ActiveRecord::Rollback if performed?
|
||||
|
||||
requested_keys_set = requested_keys.to_set
|
||||
@@ -367,7 +401,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(conflicts, row_indexes_by_key, tag)
|
||||
render_deerjikist_conflicts(locale, conflicts, row_indexes_by_key, tag)
|
||||
end
|
||||
raise ActiveRecord::Rollback if performed?
|
||||
end
|
||||
@@ -375,22 +409,28 @@ class TagsController < ApplicationController
|
||||
|
||||
return if performed?
|
||||
|
||||
render json: DeerjikistRepr.many(tag.reload.deerjikists)
|
||||
render json: DeerjikistRepr.many(tag.reload.deerjikists,
|
||||
language_code: locale.language_code)
|
||||
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_name)
|
||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||
.find_by(tag_names: { name: })
|
||||
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 head :not_found unless tag
|
||||
|
||||
graph = build_with_depth_graph(material_filter)
|
||||
graph = build_with_depth_graph(material_filter, locale.language_code)
|
||||
|
||||
render json: build_tag_children(tag, graph:)
|
||||
render json: build_tag_children(tag, graph:, language_code: locale.language_code)
|
||||
end
|
||||
|
||||
def update_all
|
||||
@@ -406,7 +446,9 @@ class TagsController < ApplicationController
|
||||
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
|
||||
return render_unprocessable_entity '廃止状態は必須です.', field: :deprecated unless params.key?(:deprecated)
|
||||
|
||||
return unless validate_tag_rename(tag, name)
|
||||
locale = resolve_locale!
|
||||
|
||||
return unless validate_tag_rename(locale, tag, name)
|
||||
|
||||
alias_names = params[:aliases].to_s.split.uniq
|
||||
parent_names = params[:parent_tags].to_s.split.uniq
|
||||
@@ -417,24 +459,25 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
|
||||
old_name = tag.name
|
||||
old_name = tag.name(locale.language_code)
|
||||
name_changed = name != old_name
|
||||
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||
wiki_page = tag.tag_name(locale.language_code)&.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!(tag, name) if name_changed
|
||||
rename_tag_name!(locale, tag, name) if name_changed
|
||||
|
||||
alias_names << old_name if name_changed
|
||||
alias_names << old_name if name_changed && old_name.present?
|
||||
alias_names.delete(name)
|
||||
|
||||
update_aliases!(tag, alias_names)
|
||||
update_parent_tags!(tag, parent_names)
|
||||
update_aliases!(locale, tag, alias_names)
|
||||
update_parent_tags!(locale, tag, parent_names)
|
||||
|
||||
tag.reload
|
||||
|
||||
@@ -442,17 +485,20 @@ 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)
|
||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
||||
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)
|
||||
@@ -460,20 +506,21 @@ class TagsController < ApplicationController
|
||||
|
||||
tag = Tag.find(params[:id])
|
||||
|
||||
return unless validate_tag_rename(tag, name)
|
||||
return unless validate_tag_rename(locale, 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)
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
|
||||
old_name = tag.name
|
||||
old_name = tag.name(locale.language_code)
|
||||
name_changed = name.present? && name != old_name
|
||||
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||
wiki_page = tag.tag_name(locale.language_code)&.wiki_page if name_changed
|
||||
|
||||
rename_tag_name!(tag, name) if name_changed
|
||||
rename_tag_name!(locale, 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)
|
||||
@@ -485,11 +532,12 @@ 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)
|
||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -501,7 +549,7 @@ class TagsController < ApplicationController
|
||||
value
|
||||
end
|
||||
|
||||
def build_with_depth_graph material_filter
|
||||
def build_with_depth_graph material_filter, language_code
|
||||
children_by_parent_id = Hash.new { |h, k| h[k] = [] }
|
||||
parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] }
|
||||
|
||||
@@ -516,10 +564,13 @@ 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_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|
|
||||
tags_by_id =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(id: tag_ids,
|
||||
tag_names: { language_code:, primary_flg: true })
|
||||
.pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
|
||||
.each_with_object({ }) do |(id, name, category, deprecated_at), h|
|
||||
h[id] = { name:, category:, deprecated: deprecated_at.present?,
|
||||
has_material: material_tag_ids.include?(id) }
|
||||
end
|
||||
@@ -651,29 +702,33 @@ class TagsController < ApplicationController
|
||||
children: [] }
|
||||
end
|
||||
|
||||
def build_tag_children tag, graph: nil
|
||||
def build_tag_children tag, graph: nil, language_code: 'ja'
|
||||
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(&:name)
|
||||
children = tag.children.sort_by { _1.name(language_code) }
|
||||
if graph
|
||||
children = children.filter { |child_tag| visible_subtree?(child_tag.id, graph) }
|
||||
end
|
||||
|
||||
TagRepr.base(tag).merge(
|
||||
children: children.map { build_tag_children(_1, graph:) },
|
||||
TagRepr.base(tag, language_code:).merge(
|
||||
children: children.map { build_tag_children(_1, graph:, language_code:) },
|
||||
has_material: tag_graph ? tag_graph[:has_material] : material.present?,
|
||||
material: material && MaterialRepr.base(material, host: request.base_url))
|
||||
material: material && MaterialRepr.base(
|
||||
material,
|
||||
host: request.base_url,
|
||||
language_code:))
|
||||
end
|
||||
|
||||
def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
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:)
|
||||
|
||||
return unless name_changed
|
||||
|
||||
wiki_page ||= tag.tag_name.wiki_page
|
||||
wiki_page ||= tag.tag_name('ja')&.wiki_page
|
||||
return unless wiki_page&.wiki_versions&.exists?
|
||||
|
||||
WikiVersionRecorder.record!(
|
||||
@@ -682,30 +737,43 @@ class TagsController < ApplicationController
|
||||
created_by_user:)
|
||||
end
|
||||
|
||||
def validate_tag_rename tag, name
|
||||
return true if name.blank? || name == tag.name
|
||||
def validate_tag_rename locale, tag, name
|
||||
return true if name.blank? || name == tag.name(locale.language_code)
|
||||
|
||||
if tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico])
|
||||
system_tag_names = ['タグ希望', 'bot操作', 'ニジラー情報不詳', '動画', 'ニコニコ']
|
||||
if TagName.exists?(tag_id: tag.id,
|
||||
language_code: 'ja',
|
||||
primary_flg: true,
|
||||
name: system_tag_names)
|
||||
render_unprocessable_entity 'システム・タグの名称は変更できません.', field: :name
|
||||
return false
|
||||
end
|
||||
|
||||
target_tag_name = TagName.find_by(name:)
|
||||
return true if target_tag_name.nil?
|
||||
return true if target_tag_name.canonical_id?
|
||||
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
||||
return true unless target_tag_name&.primary_flg
|
||||
|
||||
render_unprocessable_entity 'その名前は既に使はれてゐます.', field: :name
|
||||
false
|
||||
end
|
||||
|
||||
def rename_tag_name! tag, name
|
||||
return if name == tag.name
|
||||
def rename_tag_name! locale, tag, name
|
||||
return if name == tag.name(locale.language_code)
|
||||
|
||||
current_tag_name = tag.tag_name
|
||||
target_tag_name = TagName.find_by(name:)
|
||||
current_tag_name = tag.tag_name(locale.language_code)
|
||||
target_tag_name = TagName.find_by(language_code: locale.language_code, name:)
|
||||
|
||||
if target_tag_name.nil?
|
||||
current_tag_name.update!(name:)
|
||||
unless target_tag_name
|
||||
if current_tag_name
|
||||
current_tag_name.update!(name:, auto_generated: false)
|
||||
else
|
||||
current_tag_name = TagName.create!(
|
||||
tag:,
|
||||
language_code: locale.language_code,
|
||||
script_code: locale.script_code,
|
||||
name:,
|
||||
primary_flg: true)
|
||||
tag.association(:tag_names).reset
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
@@ -713,72 +781,96 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
|
||||
old_owner_tag = promoted_tag_name.canonical&.tag
|
||||
old_owner_tag = promoted_tag_name.tag
|
||||
|
||||
if old_owner_tag && old_owner_tag != tag
|
||||
TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
|
||||
end
|
||||
|
||||
promoted_tag_name.update!(canonical: nil)
|
||||
current_tag_name.wiki_page&.update!(tag_name_id: promoted_tag_name.id)
|
||||
current_tag_name.association(:wiki_page).reset
|
||||
|
||||
TagName.where(canonical_id: current_tag_name.id)
|
||||
current_tag_name.update!(canonical_id: promoted_tag_name.id,
|
||||
primary_flg: false, auto_generated: false)
|
||||
promoted_tag_name.update!(tag:, canonical_id: nil, primary_flg: true, auto_generated: false)
|
||||
|
||||
TagName.where(tag:,
|
||||
language_code: current_tag_name.language_code,
|
||||
primary_flg: false)
|
||||
.where.not(id: promoted_tag_name.id)
|
||||
.find_each do |alias_tag_name|
|
||||
alias_tag_name.update!(canonical: promoted_tag_name)
|
||||
alias_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
|
||||
end
|
||||
|
||||
current_tag_name.wiki_page&.update!(tag_name: promoted_tag_name)
|
||||
tag.update!(tag_name: promoted_tag_name)
|
||||
tag.update!(tag_name_id: promoted_tag_name.id)
|
||||
current_tag_name.association(:wiki_page).reset
|
||||
current_tag_name.association(:tag).reset
|
||||
current_tag_name.reload.update!(canonical: promoted_tag_name)
|
||||
current_tag_name.reload.update!(canonical_id: promoted_tag_name.id, primary_flg: false)
|
||||
|
||||
return unless old_owner_tag && old_owner_tag != tag
|
||||
|
||||
record_tag_version!(old_owner_tag.reload, event_type: :update, created_by_user: current_user)
|
||||
record_tag_version!(old_owner_tag.reload, event_type: :update,
|
||||
created_by_user: current_user,
|
||||
language_code: current_tag_name.language_code)
|
||||
end
|
||||
|
||||
def update_aliases! tag, alias_names
|
||||
def update_aliases! locale, tag, alias_names
|
||||
alias_names = alias_names.uniq
|
||||
|
||||
affected_tags = [tag]
|
||||
|
||||
current_aliases = tag.tag_name.aliases.to_a
|
||||
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.each do |alias_tag_name|
|
||||
next if alias_names.include?(alias_tag_name.name)
|
||||
|
||||
affected_tags << alias_tag_name.canonical&.tag
|
||||
affected_tags << alias_tag_name.tag
|
||||
end
|
||||
|
||||
alias_names.each do |alias_name|
|
||||
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
||||
affected_tags << alias_tag_name.canonical&.tag
|
||||
alias_tag_name = TagName.find_by(language_code: locale.language_code, name: alias_name)
|
||||
if alias_tag_name&.primary? && alias_tag_name.tag_id && alias_tag_name.tag_id != tag.id
|
||||
alias_tag_name.errors.add(:name, 'その名前は既に使はれてゐます.')
|
||||
raise ActiveRecord::RecordInvalid, alias_tag_name
|
||||
end
|
||||
affected_tags << alias_tag_name&.tag
|
||||
end
|
||||
|
||||
affected_tags.compact.uniq.each do |affected_tag|
|
||||
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user)
|
||||
TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
end
|
||||
|
||||
current_aliases.each do |alias_tag_name|
|
||||
next if alias_names.include?(alias_tag_name.name)
|
||||
|
||||
alias_tag_name.update!(canonical: nil)
|
||||
alias_tag_name.update!(canonical_id: nil, tag_id: nil)
|
||||
end
|
||||
|
||||
alias_names.each do |alias_name|
|
||||
alias_tag_name = TagName.find_or_create_by!(name: alias_name)
|
||||
alias_tag_name.update!(canonical: tag.tag_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, auto_generated: false,
|
||||
canonical_id: primary_tag_name.id)
|
||||
end
|
||||
|
||||
affected_tags.compact.uniq.each do |affected_tag|
|
||||
record_tag_version!(affected_tag, event_type: :update, created_by_user: current_user)
|
||||
record_tag_version!(affected_tag, event_type: :update,
|
||||
created_by_user: current_user,
|
||||
language_code: locale.language_code)
|
||||
end
|
||||
end
|
||||
|
||||
def update_parent_tags! tag, parent_names
|
||||
parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
def update_parent_tags! locale, tag, parent_names
|
||||
parent_tags = Tag.normalise_tags!(locale, parent_names,
|
||||
with_tagme: false,
|
||||
with_no_deerjikist: false)
|
||||
|
||||
old_parent_tags = tag.parents.to_a
|
||||
|
||||
@@ -851,15 +943,17 @@ class TagsController < ApplicationController
|
||||
.to_a
|
||||
end
|
||||
|
||||
def render_deerjikist_conflicts deerjikists, row_indexes_by_key, tag
|
||||
def render_deerjikist_conflicts locale, deerjikists, row_indexes_by_key, tag
|
||||
conflicts = deerjikists.filter { |deerjikist| deerjikist.tag_id != tag.id }
|
||||
return if conflicts.empty?
|
||||
|
||||
tag_names_by_id = Tag
|
||||
.joins(:tag_name)
|
||||
.where(id: conflicts.map(&:tag_id).uniq)
|
||||
.pluck('tags.id', 'tag_names.name')
|
||||
.to_h
|
||||
tag_names_by_id =
|
||||
Tag
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: locale.language_code, primary_flg: true },
|
||||
id: conflicts.map(&:tag_id).uniq)
|
||||
.pluck('tags.id', 'tag_names.name')
|
||||
.to_h
|
||||
fields = { }
|
||||
|
||||
conflicts.each do |deerjikist|
|
||||
|
||||
@@ -6,7 +6,7 @@ class TheatreSkipEventsController < ApplicationController
|
||||
events =
|
||||
TheatreSkipEvent
|
||||
.where(theatre_id: params[:theatre_id])
|
||||
.includes(:post, tags: :tag_name)
|
||||
.includes(:post, tags: :tag_names)
|
||||
.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 } },
|
||||
tags: event.tags.map { |tag| { id: tag.id, name: tag.name('ja') } },
|
||||
programme_position: event.programme_position,
|
||||
created_at: event.created_at }
|
||||
}
|
||||
|
||||
@@ -94,7 +94,11 @@ 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!(name: title)
|
||||
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
|
||||
|
||||
page =
|
||||
Wiki::Commit.create_content!(
|
||||
|
||||
@@ -14,7 +14,11 @@ class ExternalTag < ApplicationRecord
|
||||
|
||||
has_many :linked_tags, through: :nico_tag_relations, source: :tag
|
||||
|
||||
def snapshot_linked_tag_names
|
||||
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
def snapshot_linked_tag_names language_code = 'ja'
|
||||
linked_tags
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.order('tag_names.name')
|
||||
.pluck('tag_names.name')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class Language < ApplicationRecord
|
||||
has_many :locales, foreign_key: :language_code, primary_key: :code
|
||||
|
||||
enum :code, ja: 'ja'
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
class Locale < ApplicationRecord
|
||||
after_create_commit :generate_tag_names!
|
||||
|
||||
belongs_to :language, foreign_key: :language_code, primary_key: :code
|
||||
belongs_to :script, foreign_key: :script_code, primary_key: :code
|
||||
|
||||
enum :code, ja: 'ja', en: 'en', fr: 'fr'
|
||||
|
||||
def self.nipponese = Locale.find('ja')
|
||||
|
||||
private
|
||||
|
||||
def generate_tag_names!
|
||||
ApplicationRecord.transaction do
|
||||
tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id)
|
||||
Tag.where.not(id: tag_ids).find_each do
|
||||
TagName.create!(tag_id: _1.id,
|
||||
language_code:,
|
||||
name: TagName.generate_name(self, _1, _1.tag_name('ja')),
|
||||
script_code:,
|
||||
primary_flg: true,
|
||||
auto_generated: true,
|
||||
canonical_id: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -126,13 +126,14 @@ class Post < ApplicationRecord
|
||||
super(options).merge(thumbnail: nil)
|
||||
end
|
||||
|
||||
def snapshot_tag_names
|
||||
def snapshot_tag_names language_code = 'ja'
|
||||
post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.joins(tag: :tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.order('tag_names.name')
|
||||
.map do |post_tag|
|
||||
name = post_tag.tag.tag_name.name
|
||||
name = post_tag.tag.name(language_code)
|
||||
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||
|
||||
next name if sections.empty?
|
||||
@@ -152,16 +153,17 @@ class Post < ApplicationRecord
|
||||
"#{ tag.fetch('name') }#{ sections.join }"
|
||||
end
|
||||
|
||||
def snapshot_tags_json
|
||||
def snapshot_tags_json language_code = 'ja'
|
||||
tag_snapshots =
|
||||
post_tags
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.joins(tag: :tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.includes(:sections, tag: :tag_names)
|
||||
.order('tags.id')
|
||||
.map { |pt|
|
||||
{ 'tag_id' => pt.tag.id,
|
||||
'version_no' => pt.tag.version_no,
|
||||
'name' => pt.tag.name,
|
||||
'name' => pt.tag.name(language_code),
|
||||
'category' => pt.tag.category,
|
||||
'sections' => pt.sections.sort_by(&:begin_ms).map {
|
||||
{ 'begin_ms' => _1.begin_ms, 'end_ms' => _1.end_ms }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
class Region < ApplicationRecord
|
||||
;
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class Script < ApplicationRecord
|
||||
has_many :locales, foreign_key: :script_code, primary_key: :code
|
||||
|
||||
enum :code, Jpan: 'Jpan'
|
||||
end
|
||||
+286
-234
@@ -50,14 +50,10 @@ class Tag < ApplicationRecord
|
||||
|
||||
has_many :tag_versions
|
||||
|
||||
belongs_to :tag_name
|
||||
delegate :wiki_page, to: :tag_name
|
||||
has_many :tag_names
|
||||
|
||||
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',
|
||||
@@ -67,9 +63,8 @@ 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,
|
||||
@@ -83,8 +78,20 @@ class Tag < ApplicationRecord
|
||||
'mtr:' => :material,
|
||||
'meta:' => :meta }.freeze
|
||||
|
||||
def name= val
|
||||
(self.tag_name ||= build_tag_name).name = val
|
||||
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)
|
||||
end
|
||||
|
||||
def deprecated? = deprecated_at?
|
||||
@@ -95,214 +102,285 @@ class Tag < ApplicationRecord
|
||||
|
||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||
|
||||
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! tag_names, with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
end
|
||||
|
||||
sections = { }
|
||||
tags = tag_names.map do |name|
|
||||
raw_name = name
|
||||
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil]
|
||||
|
||||
name = name.sub(/\A#{ pf }/i, '')
|
||||
|
||||
sections_by_tag = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
sections_by_tag << normalise_section_range!(
|
||||
begin_raw: match[2],
|
||||
end_raw: match[3],
|
||||
tag_name: name)
|
||||
end
|
||||
|
||||
if name.include?('[') || name.include?(']')
|
||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
name = TagName.canonicalise(name).first
|
||||
|
||||
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
|
||||
if deny_deprecated && tag.deprecated?
|
||||
raise DeprecatedTagNormalisationError, [tag.name]
|
||||
end
|
||||
|
||||
tag.update!(category: cat) if cat && tag.category != cat
|
||||
next if sections_by_tag.empty?
|
||||
|
||||
sections[tag.id] ||= []
|
||||
sections[tag.id].concat(sections_by_tag)
|
||||
sections[tag.id] = merge_section_ranges(sections[tag.id])
|
||||
sections.delete(tag.id) if sections[tag.id] == [[0, nil]]
|
||||
end
|
||||
end
|
||||
|
||||
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||
tags << Tag.no_deerjikist if with_no_deerjikist && tags.all? { |t| !(t.deerjikist?) }
|
||||
tags.uniq!(&:id)
|
||||
|
||||
if with_sections
|
||||
{ tags:, sections: }
|
||||
else
|
||||
tags
|
||||
end
|
||||
def snapshot_aliases(language_code)
|
||||
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
|
||||
end
|
||||
|
||||
def self.expand_parent_tags tags
|
||||
return [] if tags.blank?
|
||||
|
||||
seen = Set.new
|
||||
result = []
|
||||
stack = tags.compact.dup
|
||||
|
||||
until stack.empty?
|
||||
tag = stack.pop
|
||||
next unless tag
|
||||
|
||||
tag.parents.each do |parent|
|
||||
next if seen.include?(parent.id)
|
||||
|
||||
seen << parent.id
|
||||
result << parent
|
||||
stack << parent
|
||||
end
|
||||
end
|
||||
|
||||
(result + tags).uniq { |t| t.id }
|
||||
end
|
||||
|
||||
def self.normalise_section_range! begin_raw:, end_raw:, tag_name:
|
||||
begin_ms = begin_raw.empty? ? 0 : time_to_ms!(begin_raw, tag_name:)
|
||||
end_ms = end_raw.empty? ? nil : time_to_ms!(end_raw, tag_name:)
|
||||
|
||||
if end_ms
|
||||
begin_ms, end_ms = end_ms, begin_ms if begin_ms > end_ms
|
||||
end_ms = begin_ms + 1 if begin_ms == end_ms
|
||||
end
|
||||
|
||||
[begin_ms, end_ms]
|
||||
end
|
||||
|
||||
def self.merge_section_ranges ranges
|
||||
sorted_ranges = ranges.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
|
||||
merged = []
|
||||
|
||||
sorted_ranges.each do |begin_ms, end_ms|
|
||||
if merged.empty?
|
||||
merged << [begin_ms, end_ms]
|
||||
next
|
||||
end
|
||||
|
||||
last_begin_ms, last_end_ms = merged[-1]
|
||||
if last_end_ms.nil? || begin_ms <= last_end_ms
|
||||
merged[-1] = [last_begin_ms, merge_section_end(last_end_ms, end_ms)]
|
||||
else
|
||||
merged << [begin_ms, end_ms]
|
||||
end
|
||||
end
|
||||
|
||||
merged
|
||||
end
|
||||
|
||||
def self.merge_section_end left_end_ms, right_end_ms
|
||||
return nil if left_end_ms.nil? || right_end_ms.nil?
|
||||
|
||||
[left_end_ms, right_end_ms].max
|
||||
end
|
||||
|
||||
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?
|
||||
|
||||
Tag.find_or_create_by!(tag_name_id: tn.id) do |t|
|
||||
t.category = category
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
|
||||
def self.merge_tags! target_tag, source_tags, created_by_user: nil
|
||||
target_tag => Tag
|
||||
|
||||
affected_post_ids = Set.new
|
||||
|
||||
Tag.transaction do
|
||||
TagVersioning.ensure_snapshot!(target_tag, created_by_user:)
|
||||
|
||||
Array(source_tags).compact.uniq.each do |source_tag|
|
||||
source_tag => Tag
|
||||
|
||||
next if source_tag == target_tag
|
||||
|
||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user:)
|
||||
|
||||
source_tag.post_tags.find_each do |source_pt|
|
||||
post_id = source_pt.post_id
|
||||
affected_post_ids << post_id
|
||||
source_pt.destroy!
|
||||
unless PostTag.exists?(post_id:, tag: target_tag)
|
||||
PostTag.create!(post_id:, tag: target_tag)
|
||||
end
|
||||
end
|
||||
|
||||
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.destroy!
|
||||
|
||||
source_tag_name.update_columns(canonical_id: target_tag.tag_name_id,
|
||||
updated_at: Time.current)
|
||||
|
||||
TagVersioning.record!(target_tag, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
Post.where(id: affected_post_ids.to_a).find_each do |post|
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user:)
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
# 投稿件数を再集計
|
||||
target_tag.update_columns(post_count: PostTag.where(tag: target_tag).count)
|
||||
end
|
||||
|
||||
target_tag.reload
|
||||
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
|
||||
linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
def snapshot_linked_tag_names language_code
|
||||
linked_tags
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code:, primary_flg: true })
|
||||
.order('tag_names.name')
|
||||
.pluck('tag_names.name')
|
||||
end
|
||||
|
||||
class << self
|
||||
def tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta)
|
||||
def bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta)
|
||||
def no_deerjikist =
|
||||
find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta)
|
||||
def video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta)
|
||||
def niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta)
|
||||
def youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta)
|
||||
|
||||
def normalise_tags! locale, tag_names,
|
||||
with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
end
|
||||
|
||||
sections = { }
|
||||
tags = tag_names.map do |name|
|
||||
raw_name = name
|
||||
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil]
|
||||
|
||||
name = name.sub(/\A#{ pf }/i, '')
|
||||
|
||||
sections_by_tag = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
sections_by_tag << normalise_section_range!(
|
||||
begin_raw: match[2],
|
||||
end_raw: match[3],
|
||||
tag_name: name)
|
||||
end
|
||||
|
||||
if name.include?('[') || name.include?(']')
|
||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
name = TagName.canonicalise(locale, name).first
|
||||
|
||||
find_or_create_by_tag_name!(locale, name, category: (cat || :general)).tap do |tag|
|
||||
if deny_deprecated && tag.deprecated?
|
||||
raise DeprecatedTagNormalisationError, [tag.name(locale.language_code)]
|
||||
end
|
||||
|
||||
tag.update!(category: cat) if cat && tag.category != cat
|
||||
next if sections_by_tag.empty?
|
||||
|
||||
sections[tag.id] ||= []
|
||||
sections[tag.id].concat(sections_by_tag)
|
||||
sections[tag.id] = merge_section_ranges(sections[tag.id])
|
||||
sections.delete(tag.id) if sections[tag.id] == [[0, nil]]
|
||||
end
|
||||
end
|
||||
|
||||
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||
tags << Tag.no_deerjikist if with_no_deerjikist && tags.all? { |t| !(t.deerjikist?) }
|
||||
tags.uniq!(&:id)
|
||||
|
||||
if with_sections
|
||||
{ tags:, sections: }
|
||||
else
|
||||
tags
|
||||
end
|
||||
end
|
||||
|
||||
def expand_parent_tags tags
|
||||
return [] if tags.blank?
|
||||
|
||||
seen = Set.new
|
||||
result = []
|
||||
stack = tags.compact.dup
|
||||
|
||||
until stack.empty?
|
||||
tag = stack.pop
|
||||
next unless tag
|
||||
|
||||
tag.parents.each do |parent|
|
||||
next if seen.include?(parent.id)
|
||||
|
||||
seen << parent.id
|
||||
result << parent
|
||||
stack << parent
|
||||
end
|
||||
end
|
||||
|
||||
(result + tags).uniq { |t| t.id }
|
||||
end
|
||||
|
||||
def normalise_section_range! begin_raw:, end_raw:, tag_name:
|
||||
begin_ms = begin_raw.empty? ? 0 : time_to_ms!(begin_raw, tag_name:)
|
||||
end_ms = end_raw.empty? ? nil : time_to_ms!(end_raw, tag_name:)
|
||||
|
||||
if end_ms
|
||||
begin_ms, end_ms = end_ms, begin_ms if begin_ms > end_ms
|
||||
end_ms = begin_ms + 1 if begin_ms == end_ms
|
||||
end
|
||||
|
||||
[begin_ms, end_ms]
|
||||
end
|
||||
|
||||
def merge_section_ranges ranges
|
||||
sorted_ranges = ranges.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
|
||||
merged = []
|
||||
|
||||
sorted_ranges.each do |begin_ms, end_ms|
|
||||
if merged.empty?
|
||||
merged << [begin_ms, end_ms]
|
||||
next
|
||||
end
|
||||
|
||||
last_begin_ms, last_end_ms = merged[-1]
|
||||
if last_end_ms.nil? || begin_ms <= last_end_ms
|
||||
merged[-1] = [last_begin_ms, merge_section_end(last_end_ms, end_ms)]
|
||||
else
|
||||
merged << [begin_ms, end_ms]
|
||||
end
|
||||
end
|
||||
|
||||
merged
|
||||
end
|
||||
|
||||
def merge_section_end left_end_ms, right_end_ms
|
||||
return nil if left_end_ms.nil? || right_end_ms.nil?
|
||||
|
||||
[left_end_ms, right_end_ms].max
|
||||
end
|
||||
|
||||
def find_or_create_by_tag_name! locale, name, category:
|
||||
language_code = locale.language_code
|
||||
name = name.to_s.strip
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
tn = TagName.find_or_create_by!(language_code:, name:) do
|
||||
_1.script_code = locale.script_code
|
||||
_1.primary_flg = true
|
||||
end
|
||||
|
||||
tag = tn.tag
|
||||
next tag if tag
|
||||
|
||||
tag = Tag.create!(tag_name_id: tn.id, category:)
|
||||
tn.update!(tag:, primary_flg: true, canonical_id: nil, auto_generated: false)
|
||||
generate_tag_names!(locale, tag, tn)
|
||||
tag.association(:tag_names).reset
|
||||
|
||||
tag
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
|
||||
def merge_tags! target_tag, source_tags, created_by_user: nil
|
||||
target_tag => Tag
|
||||
|
||||
affected_post_ids = Set.new
|
||||
|
||||
Tag.transaction do
|
||||
TagVersioning.ensure_snapshot!(target_tag, created_by_user:)
|
||||
|
||||
Array(source_tags).compact.uniq.each do |source_tag|
|
||||
source_tag => Tag
|
||||
|
||||
next if source_tag == target_tag
|
||||
|
||||
TagVersioning.ensure_snapshot!(source_tag, created_by_user:)
|
||||
|
||||
source_tag.post_tags.find_each do |source_pt|
|
||||
post_id = source_pt.post_id
|
||||
affected_post_ids << post_id
|
||||
source_pt.destroy!
|
||||
unless PostTag.exists?(post_id:, tag: target_tag)
|
||||
PostTag.create!(post_id:, tag: target_tag)
|
||||
end
|
||||
end
|
||||
|
||||
source_tag_name = source_tag.tag_name('ja')
|
||||
|
||||
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,
|
||||
updated_at: Time.current)
|
||||
|
||||
TagVersioning.record!(target_tag, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
Post.where(id: affected_post_ids.to_a).find_each do |post|
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user:)
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user:)
|
||||
end
|
||||
|
||||
# 投稿件数を再集計
|
||||
target_tag.update_columns(post_count: PostTag.where(tag: target_tag).count)
|
||||
end
|
||||
|
||||
target_tag.reload
|
||||
end
|
||||
|
||||
def time_to_ms! str, tag_name:
|
||||
match =
|
||||
case str
|
||||
when /\A(?<seconds>\d+)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: nil, seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<minutes>\d+):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<hours>\d+):(?<minutes>[0-5]?\d):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: Regexp.last_match[:hours],
|
||||
minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
end
|
||||
|
||||
raise SectionLiteralParseError.new(tag_name, str) unless match
|
||||
|
||||
total_s = match[:seconds].to_i
|
||||
total_s += match[:minutes].to_i * 60 if match[:minutes]
|
||||
total_s += match[:hours].to_i * 3_600 if match[:hours]
|
||||
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_tag_names! locale, tag, base_tag_name
|
||||
ApplicationRecord.transaction do
|
||||
locales =
|
||||
Locale
|
||||
.where(deprecated_at: nil)
|
||||
.where.not(language_code: locale.language_code)
|
||||
.order(:language_code, :created_at, :script_code)
|
||||
.uniq { _1.language_code }
|
||||
locales.each do |locale|
|
||||
language_code = locale.language_code
|
||||
script_code = locale.script_code
|
||||
TagName.create!(tag_id: tag.id,
|
||||
language_code:,
|
||||
name: TagName.generate_name(locale, tag, base_tag_name),
|
||||
script_code:,
|
||||
primary_flg: true,
|
||||
auto_generated: true,
|
||||
canonical_id: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_name_mustnt_start_with_nico
|
||||
if name.to_s.downcase.start_with?('nico:')
|
||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
def tag_names_must_be_valid
|
||||
tag_names.each do |tag_name|
|
||||
next if tag_name.valid?
|
||||
|
||||
def tag_name_must_be_canonical
|
||||
if tag_name&.canonical_id?
|
||||
errors.add :tag_name, 'tag_names へは実体を示す必要があります.'
|
||||
tag_name.errors[:name].each { errors.add :name, _1 }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -311,30 +389,4 @@ class Tag < ApplicationRecord
|
||||
errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.'
|
||||
end
|
||||
end
|
||||
|
||||
def self.time_to_ms! str, tag_name:
|
||||
match =
|
||||
case str
|
||||
when /\A(?<seconds>\d+)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: nil, seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<minutes>\d+):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: nil, minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
when /\A(?<hours>\d+):(?<minutes>[0-5]?\d):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
|
||||
{ hours: Regexp.last_match[:hours],
|
||||
minutes: Regexp.last_match[:minutes],
|
||||
seconds: Regexp.last_match[:seconds],
|
||||
ms: Regexp.last_match[:ms] }
|
||||
end
|
||||
|
||||
raise SectionLiteralParseError.new(tag_name, str) unless match
|
||||
|
||||
total_s = match[:seconds].to_i
|
||||
total_s += match[:minutes].to_i * 60 if match[:minutes]
|
||||
total_s += match[:hours].to_i * 3_600 if match[:hours]
|
||||
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,43 +1,61 @@
|
||||
class TagName < ApplicationRecord
|
||||
has_one :tag
|
||||
belongs_to :language, foreign_key: :language_code, primary_key: :code
|
||||
belongs_to :tag, optional: true
|
||||
has_one :wiki_page
|
||||
|
||||
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: { scope: :language_code }
|
||||
|
||||
validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
|
||||
|
||||
validate :canonical_must_be_canonical
|
||||
validate :alias_name_must_not_have_prefix
|
||||
validate :canonical_must_not_be_present_with_tag_or_wiki_page
|
||||
validate :alias_must_not_have_wiki_page
|
||||
validate :name_must_be_sanitised
|
||||
validate :name_mustnt_start_with_nico
|
||||
validate :generated_name_must_be_reserved
|
||||
|
||||
def self.canonicalise names
|
||||
def primary? = primary_flg
|
||||
|
||||
def self.canonicalise locale, names
|
||||
names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?)
|
||||
return [] if names.blank?
|
||||
|
||||
tns = TagName.includes(:canonical).where(name: names).index_by(&:name)
|
||||
tns = TagName.where(language_code: locale.language_code, name: names).index_by(&:name)
|
||||
|
||||
names.map { |name| tns[name]&.canonical&.name || name }.uniq
|
||||
names.map { |name|
|
||||
tag_name = tns[name]
|
||||
if !tag_name || tag_name.primary? || tag_name.tag_id.nil?
|
||||
name
|
||||
else
|
||||
TagName.find_by(language_code: locale.language_code,
|
||||
tag_id: tag_name.tag_id,
|
||||
primary_flg: true)&.name || name
|
||||
end
|
||||
}.uniq
|
||||
end
|
||||
|
||||
def self.generate_name locale, tag, base_tag_name
|
||||
# TODO: 言語ごとの自動命名ロジック完成したら書く.
|
||||
|
||||
"Tag_##{ tag.id }"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def canonical_must_be_canonical
|
||||
if canonical&.canonical_id?
|
||||
errors.add :canonical, 'canonical は実体を示す必要があります.'
|
||||
def generated_name_must_be_reserved
|
||||
if !auto_generated && name.to_s.start_with?('Tag_#')
|
||||
errors.add :name, 'タグの命名規則に反してゐます.'
|
||||
end
|
||||
end
|
||||
|
||||
def alias_name_must_not_have_prefix
|
||||
if canonical_id? && name.to_s.include?(':')
|
||||
if !(primary?) && name.to_s.include?(':')
|
||||
errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.'
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_must_not_be_present_with_tag_or_wiki_page
|
||||
if canonical_id? && (tag || wiki_page)
|
||||
errors.add :canonical, 'タグもしくは Wiki の参照がある名前はエーリアスになれません.'
|
||||
def alias_must_not_have_wiki_page
|
||||
if !(primary?) && wiki_page
|
||||
errors.add :primary_flg, 'Wiki 参照がある名前はエーリアスになれません.'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,4 +64,10 @@ 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
|
||||
|
||||
@@ -15,31 +15,39 @@ class TagNameSanitisationRule < ApplicationRecord
|
||||
end
|
||||
|
||||
def apply!
|
||||
TagName.find_each do |tn|
|
||||
name = sanitise(tn.name)
|
||||
next if name == tn.name
|
||||
Language.find_each do |language|
|
||||
TagName.where(language:).find_each do |tn|
|
||||
name = sanitise(tn.name)
|
||||
next if name == tn.name
|
||||
|
||||
TagName.transaction do
|
||||
existing_tn = TagName.find_by(name:)
|
||||
if existing_tn
|
||||
existing_tn = existing_tn.canonical || existing_tn
|
||||
next if existing_tn.id == tn.id
|
||||
TagName.transaction do
|
||||
existing_tn = TagName.find_by(language:, 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
|
||||
next if existing_tn.id == tn.id
|
||||
|
||||
existing_tag = Tag.find_by(tag_name_id: existing_tn.id)
|
||||
source_tag = Tag.find_by(tag_name_id: tn.id)
|
||||
existing_tag = existing_tn.tag
|
||||
source_tag = tn.tag
|
||||
|
||||
if existing_tag
|
||||
Tag.merge_tags!(existing_tag, source_tag) if tn.tag
|
||||
elsif source_tag
|
||||
source_tag.update_columns(tag_name_id: existing_tn.id, updated_at: Time.current)
|
||||
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!
|
||||
|
||||
next
|
||||
end
|
||||
tn.destroy!
|
||||
|
||||
next
|
||||
# TagName 側の自動サニタイズを回避
|
||||
tn.update_columns(name:, updated_at: Time.current)
|
||||
end
|
||||
|
||||
# TagName 側の自動サニタイズを回避
|
||||
tn.update_columns(name:, updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,5 +11,6 @@ class TagVersion < ApplicationRecord
|
||||
meta: 'meta' }, validate: true
|
||||
|
||||
validates :name, presence: true
|
||||
validates :names, presence: true
|
||||
validates :category, presence: true
|
||||
end
|
||||
|
||||
@@ -6,11 +6,12 @@ module DeerjikistRepr
|
||||
|
||||
module_function
|
||||
|
||||
def base deerjikist
|
||||
deerjikist.as_json(BASE)
|
||||
def base deerjikist, language_code: 'ja'
|
||||
deerjikist.as_json(BASE).merge(
|
||||
tag: deerjikist.tag && TagRepr.base(deerjikist.tag, language_code:))
|
||||
end
|
||||
|
||||
def many deerjikists
|
||||
deerjikists.map { |d| base(d) }
|
||||
def many deerjikists, language_code: 'ja'
|
||||
deerjikists.map { |d| base(d, language_code:) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,14 +12,15 @@ module MaterialRepr
|
||||
|
||||
module_function
|
||||
|
||||
def base material, host:
|
||||
def base material, host:, language_code: 'ja'
|
||||
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),
|
||||
thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:),
|
||||
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
|
||||
media_kind: media_kind(material),
|
||||
file_byte_size: material.file_byte_size,
|
||||
@@ -27,11 +28,11 @@ module MaterialRepr
|
||||
export_items: export_items(material))
|
||||
end
|
||||
|
||||
def many materials, host:
|
||||
materials.map { |m| base(m, host:) }
|
||||
def many materials, host:, language_code: 'ja'
|
||||
materials.map { |m| base(m, host:, language_code:) }
|
||||
end
|
||||
|
||||
def list material, host:
|
||||
def list material, host:, language_code: 'ja'
|
||||
{ id: material.id,
|
||||
version_no: material.version_no,
|
||||
url: material.url,
|
||||
@@ -40,9 +41,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),
|
||||
tag: compact_tag(material.tag, language_code:),
|
||||
thumbnail: thumbnail_url(material, host:),
|
||||
thumbnail_fallback_text: thumbnail_fallback_text(material),
|
||||
thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:),
|
||||
thumbnail_fallback_kind: thumbnail_fallback_kind(material),
|
||||
media_kind: media_kind(material),
|
||||
content_type: material.content_type,
|
||||
@@ -53,8 +54,8 @@ module MaterialRepr
|
||||
export_items: export_items(material) }
|
||||
end
|
||||
|
||||
def list_many materials, host:
|
||||
materials.map { |m| list(m, host:) }
|
||||
def list_many materials, host:, language_code: 'ja'
|
||||
materials.map { |m| list(m, host:, language_code:) }
|
||||
end
|
||||
|
||||
def export_paths material
|
||||
@@ -79,8 +80,8 @@ module MaterialRepr
|
||||
material.thumbnail, host:)
|
||||
end
|
||||
|
||||
def thumbnail_fallback_text material
|
||||
material.tag&.name || material.created_at&.strftime('%Y-%m-%d')
|
||||
def thumbnail_fallback_text material, language_code:
|
||||
material.tag&.name(language_code) || material.created_at&.strftime('%Y-%m-%d')
|
||||
end
|
||||
|
||||
def thumbnail_fallback_kind material
|
||||
@@ -98,11 +99,11 @@ module MaterialRepr
|
||||
'file_other'
|
||||
end
|
||||
|
||||
def compact_tag tag
|
||||
def compact_tag tag, language_code:
|
||||
return nil unless tag
|
||||
|
||||
{ id: tag.id,
|
||||
name: tag.name,
|
||||
name: tag.name(language_code),
|
||||
category: tag.category,
|
||||
deprecated_at: tag.deprecated_at }
|
||||
end
|
||||
|
||||
@@ -17,23 +17,23 @@ module PostRepr
|
||||
|
||||
module_function
|
||||
|
||||
def base post, current_user = nil, host: nil
|
||||
def base post, current_user = nil, host: nil, language_code: 'ja'
|
||||
json =
|
||||
if host.present?
|
||||
common(post, host:)
|
||||
else
|
||||
common(post)
|
||||
end
|
||||
json['tags'] = tag_json(post)
|
||||
json['tags'] = tag_json(post, language_code:)
|
||||
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
|
||||
sibling_posts: { }, related: [], host: nil, language_code: 'ja'
|
||||
if host.present?
|
||||
base(post, current_user, host:).merge(
|
||||
base(post, current_user, host:, language_code:).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).merge(
|
||||
base(post, current_user, language_code:).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
|
||||
def tag_json post, language_code:
|
||||
internal_tags =
|
||||
post
|
||||
.post_tags
|
||||
.reject { _1.tag.deprecated? }
|
||||
.sort_by { _1.tag.name }
|
||||
.sort_by { TagRepr.name_for(_1.tag, language_code) }
|
||||
.map { |post_tag|
|
||||
TagRepr.inline(post_tag.tag).merge(
|
||||
TagRepr.inline(post_tag.tag, language_code:).merge(
|
||||
'children' => [],
|
||||
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
|
||||
}
|
||||
|
||||
@@ -3,18 +3,23 @@
|
||||
|
||||
module TagRepr
|
||||
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at, :deprecated_at],
|
||||
methods: [:name, :has_wiki, :material_id, :has_deerjikists] }.freeze
|
||||
methods: [:has_wiki, :material_id, :has_deerjikists] }.freeze
|
||||
|
||||
module_function
|
||||
|
||||
def base tag
|
||||
tag.as_json(BASE).merge(aliases: tag.snapshot_aliases,
|
||||
parents: tag.parents.map { _1.as_json(BASE) })
|
||||
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:) })
|
||||
end
|
||||
|
||||
def inline tag
|
||||
tag.as_json(BASE).merge(aliases: [], parents: [])
|
||||
def inline tag, language_code: 'ja'
|
||||
tag.as_json(BASE).merge(name: name_for(tag, language_code), aliases: [], parents: [])
|
||||
end
|
||||
|
||||
def many(tags) = tags.map { |t| base(t) }
|
||||
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
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@ class MaterialVersionRecorder < VersionRecorder
|
||||
{ url: @record.url,
|
||||
parent: @record.parent,
|
||||
tag: @record.tag,
|
||||
tag_name: @record.tag&.name,
|
||||
tag_name: @record.tag&.name('ja'),
|
||||
tag_category: @record.tag&.category,
|
||||
source_kind: @record.source_kind,
|
||||
source_uri: @record.source_uri,
|
||||
|
||||
@@ -6,7 +6,7 @@ class PostBulkCreator
|
||||
@host = host
|
||||
end
|
||||
|
||||
def run
|
||||
def run locale
|
||||
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(actor, attributes, index)
|
||||
results[index] = create_row(locale, 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 actor, attributes, index
|
||||
def create_row locale, actor, attributes, index
|
||||
preflight =
|
||||
PostCreatePreflight.new(
|
||||
attributes: attributes,
|
||||
thumbnail: thumbnail_for(index, attributes),
|
||||
host: @host).run
|
||||
host: @host).run(locale)
|
||||
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!
|
||||
attributes: normalised_attributes(attributes, preflight, index)).create!(locale)
|
||||
result = {
|
||||
status: 'created',
|
||||
post: { id: post.id } }
|
||||
|
||||
@@ -8,13 +8,13 @@ class PostCreatePlan
|
||||
@existing_tags_by_name = nil
|
||||
end
|
||||
|
||||
def build!
|
||||
direct_tag_specs, tag_sections = parse_direct_tag_specs
|
||||
default_tag_specs = build_default_tag_specs(direct_tag_specs)
|
||||
def build! locale
|
||||
direct_tag_specs, tag_sections = parse_direct_tag_specs(locale)
|
||||
default_tag_specs = build_default_tag_specs(locale, direct_tag_specs)
|
||||
snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_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)
|
||||
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)
|
||||
video_ms = normalise_video_ms(snapshot_tag_specs)
|
||||
validate_video_sections!(video_ms, tag_sections)
|
||||
parent_post_ids = normalise_parent_post_ids
|
||||
@@ -42,14 +42,16 @@ class PostCreatePlan
|
||||
|
||||
def tag_names = @attributes[:tags].to_s.split
|
||||
|
||||
def parse_direct_tag_specs
|
||||
def parse_direct_tag_specs locale
|
||||
tag_sections = { }
|
||||
direct_tag_specs = []
|
||||
|
||||
tag_names.each do |raw_name|
|
||||
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?
|
||||
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
|
||||
|
||||
direct_tag_specs << {
|
||||
name: tag_name,
|
||||
@@ -65,7 +67,7 @@ class PostCreatePlan
|
||||
[merge_tag_specs(direct_tag_specs), tag_sections]
|
||||
end
|
||||
|
||||
def parse_raw_tag_name raw_name
|
||||
def parse_raw_tag_name locale, raw_name
|
||||
name = raw_name.to_s
|
||||
prefix, category =
|
||||
Tag::CATEGORY_PREFIXES.find {
|
||||
@@ -74,7 +76,7 @@ class PostCreatePlan
|
||||
name = name.sub(/\A#{ prefix }/i, '')
|
||||
|
||||
sections = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
while match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/)
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
@@ -87,17 +89,17 @@ class PostCreatePlan
|
||||
raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
[resolved_tag_name(name), category&.to_sym, sections]
|
||||
[resolved_tag_name(locale, name), category&.to_sym, sections]
|
||||
end
|
||||
|
||||
def build_default_tag_specs direct_tag_specs
|
||||
def build_default_tag_specs locale, 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?(_1) }
|
||||
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
|
||||
default_tag_specs << {
|
||||
name: NO_DEERJIKIST_TAG_NAME,
|
||||
category: :meta }
|
||||
@@ -106,17 +108,18 @@ class PostCreatePlan
|
||||
default_tag_specs
|
||||
end
|
||||
|
||||
def validate_new_tag_specs! specs
|
||||
def validate_new_tag_specs! locale, specs
|
||||
Array(specs).each do |spec|
|
||||
next if existing_tags_by_name.key?(spec[:name])
|
||||
next if existing_tags_by_name(locale).key?(spec[:name])
|
||||
|
||||
validate_new_tag_spec!(spec)
|
||||
validate_new_tag_spec!(locale, spec)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_new_tag_spec! spec
|
||||
tag_name = TagName.new(name: spec[:name])
|
||||
tag = Tag.new(category: spec[:category], tag_name:)
|
||||
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])
|
||||
return if tag_name.valid? && tag.valid?
|
||||
|
||||
post = Post.new
|
||||
@@ -131,13 +134,15 @@ class PostCreatePlan
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
|
||||
def expand_parent_tag_specs snapshot_tag_specs
|
||||
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
|
||||
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
|
||||
expanded_parent_specs =
|
||||
Tag.expand_parent_tags(existing_snapshot_tags)
|
||||
.reject(&:deprecated?)
|
||||
.map { |tag|
|
||||
{ name: tag.name,
|
||||
{ name: tag.name(locale.language_code),
|
||||
category: tag.category.to_sym }
|
||||
}
|
||||
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
||||
@@ -156,33 +161,37 @@ class PostCreatePlan
|
||||
}.values.sort_by { _1[:name] }
|
||||
end
|
||||
|
||||
def existing_tags_by_name
|
||||
def existing_tags_by_name locale
|
||||
@existing_tags_by_name ||= begin
|
||||
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
|
||||
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
|
||||
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 }
|
||||
end
|
||||
end
|
||||
|
||||
def preload_existing_tags_by_name! names
|
||||
def preload_existing_tags_by_name! locale, names
|
||||
wanted_names = Array(names).map { _1.to_s }.reject(&:blank?).uniq
|
||||
missing_names = wanted_names - existing_tags_by_name.keys
|
||||
missing_names = wanted_names - existing_tags_by_name(locale).keys
|
||||
return if missing_names.empty?
|
||||
|
||||
existing_tags_by_name.merge!(
|
||||
Tag.joins(:tag_name)
|
||||
.where(tag_names: { name: missing_names })
|
||||
.index_by(&:name))
|
||||
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 })
|
||||
end
|
||||
|
||||
def canonical_tag_name_without_sections raw_name
|
||||
name, = parse_raw_tag_name(raw_name)
|
||||
def canonical_tag_name_without_sections locale, raw_name
|
||||
name, = parse_raw_tag_name(locale, raw_name)
|
||||
name
|
||||
end
|
||||
|
||||
def deerjikist_tag_spec? spec
|
||||
def deerjikist_tag_spec? locale, spec
|
||||
return true if spec[:category] == :deerjikist
|
||||
|
||||
existing_tags_by_name[spec[:name]]&.deerjikist?
|
||||
existing_tags_by_name(locale)[spec[:name]]&.deerjikist?
|
||||
end
|
||||
|
||||
def normalise_parent_post_ids
|
||||
@@ -260,10 +269,12 @@ class PostCreatePlan
|
||||
end
|
||||
end
|
||||
|
||||
def resolved_tag_name name
|
||||
tag_name = TagName.includes(:canonical).find_by(name:)
|
||||
return name if tag_name.nil?
|
||||
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?
|
||||
|
||||
(tag_name.canonical || tag_name).name
|
||||
TagName.find_by(language_code: locale.language_code,
|
||||
tag_id: tag_name.tag_id,
|
||||
primary_flg: true)&.name || name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,9 +15,10 @@ class PostCreatePreflight
|
||||
@host = host
|
||||
end
|
||||
|
||||
def run
|
||||
def run locale
|
||||
preview = PostImportPreviewer.new.preview_rows(
|
||||
rows: [preview_row],
|
||||
locale:,
|
||||
fetch_metadata: false).first
|
||||
if preview[:existing_post_id].present?
|
||||
return {
|
||||
@@ -53,7 +54,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!
|
||||
video_ms: preview[:attributes]['video_ms'] }).build!(locale)
|
||||
|
||||
{
|
||||
url: plan[:url],
|
||||
|
||||
@@ -9,7 +9,7 @@ class PostCreator
|
||||
@field_warnings = { }
|
||||
end
|
||||
|
||||
def create!
|
||||
def create! locale
|
||||
thumbnail_attachment = prepare_thumbnail_attachment
|
||||
post = Post.new(title: @attributes[:title].presence,
|
||||
url: @attributes[:url],
|
||||
@@ -21,14 +21,17 @@ class PostCreator
|
||||
ApplicationRecord.transaction do
|
||||
post.save!
|
||||
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
|
||||
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
|
||||
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)
|
||||
post.save!
|
||||
sync_post_tags!(post, post_tags, sections)
|
||||
sync_parent_posts!(post, planned_parent_post_ids)
|
||||
sync_parent_posts!(post, planned_parent_post_ids(locale))
|
||||
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
|
||||
end
|
||||
post
|
||||
@@ -45,69 +48,73 @@ class PostCreator
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence)
|
||||
end
|
||||
|
||||
def planned_snapshot_tags = planned_create_attributes[:snapshot_tags]
|
||||
def planned_snapshot_tags(locale) = planned_create_attributes(locale)[:snapshot_tags]
|
||||
|
||||
def planned_post_tags = planned_create_attributes[:post_tags]
|
||||
def planned_post_tags(locale) = planned_create_attributes(locale)[:post_tags]
|
||||
|
||||
def planned_sections = planned_create_attributes[:tag_sections]
|
||||
def planned_sections(locale) = planned_create_attributes(locale)[:tag_sections]
|
||||
|
||||
def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]
|
||||
|
||||
def planned_video_ms
|
||||
planned_create_attributes[:video_ms]
|
||||
def planned_parent_post_ids locale
|
||||
planned_create_attributes(locale)[:normalised_parent_post_ids]
|
||||
end
|
||||
|
||||
def planned_create_attributes
|
||||
def planned_video_ms locale
|
||||
planned_create_attributes(locale)[:video_ms]
|
||||
end
|
||||
|
||||
def planned_create_attributes locale
|
||||
@planned_create_attributes ||= begin
|
||||
if @attributes.key?(:snapshot_tag_specs)
|
||||
snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
|
||||
snapshot_tags = materialise_tags(locale, @attributes[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(locale, @attributes[:post_tag_specs] || [])
|
||||
{
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
@attributes[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags),
|
||||
post_tags,
|
||||
locale),
|
||||
normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
|
||||
video_ms: @attributes[:video_ms] }
|
||||
else
|
||||
build_materialised_plan
|
||||
build_materialised_plan(locale)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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] || [])
|
||||
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] || [])
|
||||
{
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
plan[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags),
|
||||
post_tags,
|
||||
locale),
|
||||
normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
|
||||
video_ms: plan[:video_ms] }
|
||||
end
|
||||
|
||||
def materialise_tags specs
|
||||
def materialise_tags locale, 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!(name, category:)
|
||||
tag = Tag.find_or_create_by_tag_name!(locale, 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
|
||||
tags_by_name = post_tags.index_by(&:name)
|
||||
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) }
|
||||
snapshot_tags.each do |tag|
|
||||
tags_by_name[tag.name] ||= tag
|
||||
tags_by_name[tag.name(locale.language_code)] ||= tag
|
||||
end
|
||||
|
||||
sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
|
||||
|
||||
@@ -16,7 +16,7 @@ class PostImportPreviewer
|
||||
THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze
|
||||
METADATA_FETCH_WARNING = '自動取得に失敗しました.'.freeze
|
||||
|
||||
def preview_rows rows:, fetch_metadata: true, metadata_cache: { }
|
||||
def preview_rows rows:, locale: Locale.nipponese, 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,13 +30,14 @@ 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(row,
|
||||
preview_row(locale, row,
|
||||
fetch_metadata:,
|
||||
metadata_cache:,
|
||||
existing_posts:,
|
||||
@@ -59,7 +60,7 @@ class PostImportPreviewer
|
||||
source.merge(url_text: url, normal_url:, url_error: validate_url_safety(normal_url))
|
||||
end
|
||||
|
||||
def preview_row row,
|
||||
def preview_row locale, row,
|
||||
fetch_metadata:,
|
||||
metadata_cache:,
|
||||
existing_posts:,
|
||||
@@ -124,7 +125,8 @@ class PostImportPreviewer
|
||||
|
||||
attributes['url'] = url
|
||||
validate_basic_data(attributes, validation_errors)
|
||||
validate_preview_tags(merged_tags(tag_sources, provenance['tags']),
|
||||
validate_preview_tags(locale,
|
||||
merged_tags(tag_sources, provenance['tags']),
|
||||
validation_errors,
|
||||
known_tags)
|
||||
validate_parents(attributes['parent_post_ids'], validation_errors, existing_parent_ids)
|
||||
@@ -346,7 +348,7 @@ class PostImportPreviewer
|
||||
{ data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } }
|
||||
end
|
||||
|
||||
def preload_known_tags prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts
|
||||
def preload_known_tags locale, prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts
|
||||
names = prepared_rows.flat_map { |row|
|
||||
attributes = initial_attributes(row)
|
||||
provenance = initial_provenance(row)
|
||||
@@ -358,15 +360,13 @@ class PostImportPreviewer
|
||||
metadata = metadata_for(row[:normal_url], metadata_cache)
|
||||
apply_metadata!(attributes, provenance, tag_sources, metadata[:data])
|
||||
end
|
||||
preview_tag_names(merged_tags(tag_sources, provenance['tags']))
|
||||
preview_tag_names(locale, merged_tags(tag_sources, provenance['tags']))
|
||||
}.compact.uniq
|
||||
return { } if names.empty?
|
||||
|
||||
Tag.joins(:tag_name)
|
||||
.where(tag_names: { name: names })
|
||||
.includes(:tag_name)
|
||||
.to_a
|
||||
.index_by(&:name)
|
||||
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 }
|
||||
end
|
||||
|
||||
def preload_parent_ids prepared_rows
|
||||
@@ -437,19 +437,21 @@ class PostImportPreviewer
|
||||
sources['automatic'].to_s
|
||||
end
|
||||
|
||||
def preview_tag_names raw
|
||||
def preview_tag_names locale, 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(name.sub(/\[.*\]\z/, '')).first }
|
||||
names.map { |name|
|
||||
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
|
||||
}
|
||||
rescue Tag::SectionLiteralParseError
|
||||
[]
|
||||
end
|
||||
|
||||
def validate_preview_tags raw, errors, known_tags
|
||||
def validate_preview_tags locale, raw, errors, known_tags
|
||||
names = raw.to_s.split
|
||||
return if names.empty?
|
||||
if names.any? { _1.downcase.start_with?('nico:') }
|
||||
@@ -457,9 +459,11 @@ class PostImportPreviewer
|
||||
return
|
||||
end
|
||||
|
||||
parsed = names.map { |name| TagName.canonicalise(name.sub(/\[.*\]\z/, '')).first }
|
||||
parsed = names.map { |name|
|
||||
TagName.canonicalise(locale, name.sub(/\[.*\]\z/, '')).first
|
||||
}
|
||||
existing = parsed.filter_map { known_tags[_1] }
|
||||
deprecated = existing.select(&:deprecated?).map(&:name)
|
||||
deprecated = existing.select(&:deprecated?).map { _1.name(locale.language_code) }
|
||||
errors[:tags] = ["廃止済みタグがあります: #{ deprecated.join(' ') }"] if deprecated.present?
|
||||
rescue Tag::SectionLiteralParseError
|
||||
errors[:tags] = ['タグ区間の記法が不正です.']
|
||||
|
||||
@@ -47,9 +47,9 @@ class PostMetadataFetcher
|
||||
|
||||
existing_tags =
|
||||
Tag
|
||||
.joins(:tag_name)
|
||||
.where(tag_names: { name: names })
|
||||
.index_by(&:name)
|
||||
.joins(:tag_names)
|
||||
.where(tag_names: { language_code: 'ja', primary_flg: true, name: names })
|
||||
.index_by { _1.name('ja') }
|
||||
|
||||
names.map { |name|
|
||||
tag = existing_tags[name]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class TagVersionRecorder < VersionRecorder
|
||||
def self.record! tag:, event_type:, created_by_user:
|
||||
def self.record! tag:, event_type:, created_by_user:, **_options
|
||||
new(tag:, event_type:, created_by_user:).record!
|
||||
end
|
||||
|
||||
@@ -14,10 +14,29 @@ class TagVersionRecorder < VersionRecorder
|
||||
def record_key = :tag
|
||||
|
||||
def snapshot_attributes
|
||||
{ name: @record.name,
|
||||
names = snapshot_names
|
||||
compatibility_names = names.fetch('ja')
|
||||
|
||||
{ name: compatibility_names.fetch('name'),
|
||||
category: @record.category,
|
||||
deprecated_at: @record.deprecated_at,
|
||||
aliases: @record.snapshot_aliases.join(' '),
|
||||
aliases: compatibility_names.fetch('aliases').map { _1.fetch('name') }.join(' '),
|
||||
names:,
|
||||
parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') }
|
||||
end
|
||||
|
||||
def snapshot_names
|
||||
@record.tag_names.order(:language_code, :name).group_by(&:language_code)
|
||||
.transform_values do |tag_names|
|
||||
primary = tag_names.find(&:primary?)
|
||||
aliases = tag_names.reject(&:primary?).sort_by(&:name).map { |tag_name|
|
||||
{ 'name' => tag_name.name, 'script_code' => tag_name.script_code }
|
||||
}
|
||||
|
||||
{ 'name' => primary.name,
|
||||
'script_code' => primary.script_code,
|
||||
'auto_generated' => primary.auto_generated,
|
||||
'aliases' => aliases }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
class TagVersioning
|
||||
def self.record! tag, event_type:, created_by_user:
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||
def self.record! tag, event_type:, created_by_user:, language_code: 'ja'
|
||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
|
||||
end
|
||||
|
||||
def self.ensure_snapshot! tag, created_by_user:
|
||||
def self.ensure_snapshot! tag, created_by_user:, language_code: 'ja'
|
||||
return if tag.tag_versions.exists?
|
||||
|
||||
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:)
|
||||
TagVersionRecorder.record!(tag:, event_type: :create, created_by_user:, language_code:)
|
||||
end
|
||||
|
||||
def self.record_tag_snapshot! tag, created_by_user:
|
||||
def self.record_tag_snapshot! tag, created_by_user:, language_code: 'ja'
|
||||
event_type = tag.tag_versions.exists? ? :update : :create
|
||||
record!(tag, event_type:, created_by_user:)
|
||||
record!(tag, event_type:, created_by_user:, language_code:)
|
||||
end
|
||||
|
||||
def self.record_tag_snapshots! tags, created_by_user:
|
||||
def self.record_tag_snapshots! tags, created_by_user:, language_code: 'ja'
|
||||
tags.each do |tag|
|
||||
record_tag_snapshot!(tag, created_by_user:)
|
||||
record_tag_snapshot!(tag, created_by_user:, language_code:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ class TheatrePostSelector
|
||||
def weighted_candidates
|
||||
@weighted_candidates ||= begin
|
||||
penalties = tag_penalties
|
||||
posts = eligible_posts.includes(tags: :tag_name).to_a
|
||||
posts = eligible_posts.includes(tags: :tag_names).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_name).index_by(&:id)
|
||||
tags = Tag.where(id: tag_penalties.keys).includes(:tag_names).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,
|
||||
name: tag.name('ja'),
|
||||
category: tag.category }
|
||||
end
|
||||
end
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする