diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 99da557..3843bb7 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -689,6 +689,7 @@ class PostsController < ApplicationController version.tags_json .select { _1.key?('tag_id') } .map { Post.tag_snapshot_literal(_1) } + .compact .sort end @@ -716,7 +717,7 @@ class PostsController < ApplicationController next name if sections.empty? "#{ name }#{ sections.map { Post.section_literal(_1) }.join }" - end + end.compact.sort end def post_incoming_snapshot locale:, title:, original_created_from:, original_created_before:, @@ -725,6 +726,7 @@ class PostsController < ApplicationController Tag.normalise_tags!(locale, tag_names, with_tagme: false, + with_no_deerjikist: false, deny_deprecated: true, with_sections: true) => { tags:, sections: } diff --git a/backend/app/controllers/tag_versions_controller.rb b/backend/app/controllers/tag_versions_controller.rb index 6709381..2aab1db 100644 --- a/backend/app/controllers/tag_versions_controller.rb +++ b/backend/app/controllers/tag_versions_controller.rb @@ -19,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 @@ -38,8 +39,10 @@ class TagVersionsController < ApplicationController 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.reject { _1['primary_flg'] }.map { _1['name'] } + prev_aliases = previous_names.reject { _1['primary_flg'] }.map { _1['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']) @@ -63,7 +66,8 @@ class TagVersionsController < ApplicationController version_no: row.version_no, event_type: row.event_type, names: row.names, - name: { current: row.name, prev: row.attributes['prev_name'] }, + name: { current: current_names.find { _1['primary_flg'] }&.fetch('name'), + prev: previous_names.find { _1['primary_flg'] }&.fetch('name') }, category: { current: row.category, prev: row.attributes['prev_category'] }, deprecated_at: { current: row.deprecated_at&.iso8601, prev: row.attributes['prev_deprecated_at']&.iso8601 }, @@ -93,5 +97,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) : Array(names) + values.select { _1['language_code'] == language_code } + rescue JSON::ParserError + [] + end + def split_parent_tag_ids(values) = split_values(values).map(&:to_i) end diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index 53f738e..58df873 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -473,7 +473,7 @@ class TagsController < ApplicationController end 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!(locale, tag, alias_names) @@ -772,6 +772,7 @@ class TagsController < ApplicationController script_code: locale.script_code, name:, primary_flg: true) + tag.association(:tag_names).reset end return end @@ -786,6 +787,9 @@ class TagsController < ApplicationController TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user) end + current_tag_name.wiki_page&.update!(tag_name_id: promoted_tag_name.id) + current_tag_name.association(:wiki_page).reset + current_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false) promoted_tag_name.update!(tag:, canonical_id: nil, primary_flg: true) @@ -797,7 +801,6 @@ class TagsController < ApplicationController alias_tag_name.update!(canonical_id: promoted_tag_name.id, primary_flg: false) end - current_tag_name.wiki_page&.update!(tag_name_id: promoted_tag_name.id) tag.update!(tag_name_id: promoted_tag_name.id) current_tag_name.association(:wiki_page).reset current_tag_name.association(:tag).reset @@ -815,7 +818,9 @@ class TagsController < ApplicationController affected_tags = [tag] - primary_tag_name = tag.tag_name(locale.language_code) + 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 diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index 9b38b0e..b0d0b8c 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -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!( diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index e449781..fa659ee 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -64,6 +64,7 @@ class Tag < ApplicationRecord validates :category, presence: true, inclusion: { in: Tag.categories.keys } validate :category_must_be_deerjikist_with_deerjikists + validate :tag_names_must_be_valid CATEGORY_PREFIXES = { 'general:' => :general, @@ -77,10 +78,22 @@ class Tag < ApplicationRecord 'mtr:' => :material, 'meta:' => :meta }.freeze - def tag_name(language_code) = tag_names.find_by(language_code:, primary_flg: true) + 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? def has_wiki = wiki_page.present? @@ -232,6 +245,7 @@ class Tag < ApplicationRecord tag = Tag.create!(tag_name_id: tn.id, category:) tn.update!(tag:) + tag.association(:tag_names).reset tag rescue ActiveRecord::RecordNotUnique @@ -307,6 +321,14 @@ class Tag < ApplicationRecord private + def tag_names_must_be_valid + tag_names.each do |tag_name| + next if tag_name.valid? + + tag_name.errors[:name].each { errors.add :name, _1 } + end + end + def category_must_be_deerjikist_with_deerjikists if !(deerjikist?) && deerjikists.exists? errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.' diff --git a/backend/app/representations/post_repr.rb b/backend/app/representations/post_repr.rb index abd0dd2..4784526 100644 --- a/backend/app/representations/post_repr.rb +++ b/backend/app/representations/post_repr.rb @@ -91,7 +91,7 @@ module PostRepr post .post_tags .reject { _1.tag.deprecated? } - .sort_by { _1.tag.name(language_code) } + .sort_by { TagRepr.name_for(_1.tag, language_code) } .map { |post_tag| TagRepr.inline(post_tag.tag, language_code:).merge( 'children' => [], diff --git a/backend/app/representations/tag_repr.rb b/backend/app/representations/tag_repr.rb index d54e743..9e3a42e 100644 --- a/backend/app/representations/tag_repr.rb +++ b/backend/app/representations/tag_repr.rb @@ -8,14 +8,18 @@ module TagRepr module_function def base tag, language_code: 'ja' - tag.as_json(BASE).merge(name: tag.name(language_code), + 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, language_code: 'ja' - tag.as_json(BASE).merge(name: tag.name(language_code), aliases: [], parents: []) + tag.as_json(BASE).merge(name: name_for(tag, language_code), aliases: [], parents: []) end def many(tags, language_code: 'ja') = tags.map { |tag| base(tag, language_code:) } + + def name_for tag, language_code + tag.name(language_code) || (tag.system? ? tag.name('ja') : nil) + end end diff --git a/backend/app/services/post_import_previewer.rb b/backend/app/services/post_import_previewer.rb index a81a6b6..2431ded 100644 --- a/backend/app/services/post_import_previewer.rb +++ b/backend/app/services/post_import_previewer.rb @@ -463,7 +463,7 @@ class PostImportPreviewer 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] = ['タグ区間の記法が不正です.'] diff --git a/backend/app/services/tag_version_recorder.rb b/backend/app/services/tag_version_recorder.rb index d5debc8..025c3d4 100644 --- a/backend/app/services/tag_version_recorder.rb +++ b/backend/app/services/tag_version_recorder.rb @@ -15,23 +15,24 @@ class TagVersionRecorder < VersionRecorder def snapshot_attributes names = snapshot_names - japanese_names = names.select { _1[:language_code] == 'ja' } + japanese_names = names.select { _1['language_code'] == 'ja' } compatibility_names = japanese_names.presence || names - { name: compatibility_names.find { _1[:primary_flg] }[:name], + { name: compatibility_names.find { _1['primary_flg'] }['name'], category: @record.category, deprecated_at: @record.deprecated_at, - aliases: compatibility_names.reject { _1[:primary_flg] }.map { _1[:name] }.join(' '), + aliases: compatibility_names.reject { _1['primary_flg'] }.map { _1['name'] }.join(' '), names:, parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') } end def snapshot_names @record.tag_names.order(:language_code, primary_flg: :desc).order(:name).map { |tag_name| - { language_code: tag_name.language_code, - name: tag_name.name, - script_code: tag_name.script_code, - primary_flg: tag_name.primary_flg } + { 'language_code' => tag_name.language_code, + 'name' => tag_name.name, + 'script_code' => tag_name.script_code, + 'primary_flg' => tag_name.primary_flg, + 'auto_generated' => tag_name.auto_generated } } end end diff --git a/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb b/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb index 4a84eb2..c64f9eb 100644 --- a/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb +++ b/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb @@ -13,7 +13,8 @@ class AddNamesToTagVersions < ActiveRecord::Migration[8.0] language_code: 'ja', name: version.name, script_code: 'Jpan', - primary_flg: true }] + primary_flg: true, + auto_generated: false }] names.concat(version.aliases.to_s.split.filter_map { |name| next if name.blank? @@ -21,7 +22,8 @@ class AddNamesToTagVersions < ActiveRecord::Migration[8.0] language_code: 'ja', name:, script_code: 'Jpan', - primary_flg: false } + primary_flg: false, + auto_generated: false } }) version.update_columns(names: names) end diff --git a/backend/db/migrate/20260922050000_add_auto_generated_to_tag_names.rb b/backend/db/migrate/20260922050000_add_auto_generated_to_tag_names.rb new file mode 100644 index 0000000..79cb3cc --- /dev/null +++ b/backend/db/migrate/20260922050000_add_auto_generated_to_tag_names.rb @@ -0,0 +1,19 @@ +class AddAutoGeneratedToTagNames < ActiveRecord::Migration[8.0] + class TagVersion < ActiveRecord::Base + self.table_name = 'tag_versions' + end + + def up + add_column :tag_names, :auto_generated, :boolean, null: false, default: false + TagVersion.reset_column_information + + TagVersion.find_each do |version| + names = Array(version.names).map { |name| name.merge('auto_generated' => false) } + version.update_columns(names: names) + end + end + + def down + remove_column :tag_names, :auto_generated + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 8862e73..bab1a8e 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do +ActiveRecord::Schema[8.0].define(version: 2026_09_22_050000) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -454,6 +454,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.virtual "primary_tag_id", type: :bigint, as: "(case when `primary_flg` then `tag_id` else NULL end)" + t.boolean "auto_generated", default: false, null: false t.index ["canonical_id"], name: "index_tag_names_on_canonical_id" t.index ["language_code", "name"], name: "index_tag_names_on_language_code_and_name", unique: true t.index ["primary_tag_id", "language_code"], name: "index_tag_names_on_primary_tag_id_and_language_code", unique: true diff --git a/backend/spec/factories/tag_names.rb b/backend/spec/factories/tag_names.rb index f4d6890..0e98e3b 100644 --- a/backend/spec/factories/tag_names.rb +++ b/backend/spec/factories/tag_names.rb @@ -3,6 +3,7 @@ FactoryBot.define do language_code { 'ja' } script_code { 'Jpan' } primary_flg { true } + auto_generated { false } sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" } trait :alias do diff --git a/backend/spec/services/tag_version_recorder_spec.rb b/backend/spec/services/tag_version_recorder_spec.rb index 9bec5d7..f97562c 100644 --- a/backend/spec/services/tag_version_recorder_spec.rb +++ b/backend/spec/services/tag_version_recorder_spec.rb @@ -23,13 +23,13 @@ RSpec.describe TagVersionRecorder do expect(version.names).to eq([ { 'language_code' => 'en', 'name' => 'English name', 'script_code' => 'Latn', - 'primary_flg' => true }, + 'primary_flg' => true, 'auto_generated' => false }, { 'language_code' => 'en', 'name' => 'English alias', 'script_code' => 'Latn', - 'primary_flg' => false }, + 'primary_flg' => false, 'auto_generated' => false }, { 'language_code' => 'ja', 'name' => '日本語名', 'script_code' => 'Jpan', - 'primary_flg' => true }, + 'primary_flg' => true, 'auto_generated' => false }, { 'language_code' => 'ja', 'name' => '別名', 'script_code' => 'Jpan', - 'primary_flg' => false } + 'primary_flg' => false, 'auto_generated' => false } ]) end @@ -44,4 +44,13 @@ RSpec.describe TagVersionRecorder do hash_including('language_code' => 'en', 'name' => 'English name')) expect(updated.names).to include(hash_including('language_code' => 'en', 'name' => 'Renamed')) end + + it 'does not create a version for an unchanged multilingual snapshot' do + described_class.record!(tag:, event_type: :create, created_by_user: user) + + unchanged = described_class.record!(tag:, event_type: :update, created_by_user: user) + + expect(unchanged.version_no).to eq(1) + expect(tag.reload.tag_versions.count).to eq(1) + end end