From b0df11c5008d3f354fcf140583b1c1fa4c001b98 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Mon, 21 Sep 2026 01:27:10 +0900 Subject: [PATCH] #55 --- .../app/controllers/materials_controller.rb | 8 +-- backend/app/controllers/posts_controller.rb | 22 +++--- backend/app/models/locale.rb | 22 +++++- backend/app/models/tag.rb | 47 +++++++----- backend/app/models/tag_name.rb | 8 ++- backend/db/schema.rb | 71 ++++--------------- 6 files changed, 87 insertions(+), 91 deletions(-) diff --git a/backend/app/controllers/materials_controller.rb b/backend/app/controllers/materials_controller.rb index 17cd4d0..3383806 100644 --- a/backend/app/controllers/materials_controller.rb +++ b/backend/app/controllers/materials_controller.rb @@ -236,10 +236,10 @@ class MaterialsController < ApplicationController nil end - def resolve_material_tag! tag_name_raw - tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw) - tag = tag_name.tag - tag || Tag.create!(tag_name:, category: :material) + def resolve_material_tag! locale, tag_name_raw + tag_name = TagName.find_undiscard_or_create_by!(language_code: locale.language_code, + name: tag_name_raw) + tag_name.tag || Tag.create!(tag_name:, category: :material) end def material_index_needs_tag_name? filters diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 3f5a92c..d59a725 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -311,6 +311,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] @@ -332,7 +333,8 @@ class PostsController < ApplicationController base_snapshot = post_snapshot_from_version(base_version) current_snapshot = post_snapshot_from_record(post) end - incoming_snapshot = post_incoming_snapshot(title:, + incoming_snapshot = post_incoming_snapshot(locale:, + title:, original_created_from:, original_created_before:, tag_names:, @@ -361,7 +363,7 @@ 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 @@ -733,11 +735,11 @@ class PostsController < ApplicationController end 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) => + Tag.normalise_tags!(locale, tag_names, with_tagme: false, deny_deprecated: true, + with_sections: true) => { tags:, sections: } tags = Tag.expand_parent_tags(tags).reject(&:deprecated?) @@ -873,7 +875,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], @@ -881,10 +883,10 @@ class PostsController < ApplicationController original_created_from: snapshot[:original_created_from], original_created_before: snapshot[:original_created_before]) - Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false, - deny_deprecated: true, - with_sections: true) => - { tags: editable_tags, sections: } + Tag.normalise_tags!(locale, snapshot[:tag_names], + with_tagme: false, + deny_deprecated: true, + with_sections: true) => { tags: editable_tags, sections: } TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user) readonly_tags = post.tags.nico.to_a diff --git a/backend/app/models/locale.rb b/backend/app/models/locale.rb index 06b644b..51f4954 100644 --- a/backend/app/models/locale.rb +++ b/backend/app/models/locale.rb @@ -1,6 +1,24 @@ class Locale < ApplicationRecord - belongs_to :language_code, class_name: 'Language', foreign_key: :code - belongs_to :script_code, class_name: 'Script', foreign_key: :code + after_create_commit :generate_tag_names! + + belongs_to :language, foreign_key: :language_code, primary_key: :code + belongs_to :script, foreign_key: :script_code, primary_key: :code def self.nipponese = Locale.find('ja') + + private + + def generate_tag_names! + tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id) + Tag.where.not(id: tag_ids).find_each do + TagName.create!(tag_id: _1.id, + language_code:, + name: TagName.generate_name(self, _1, _1.name), + script_code:, + primary_flg: true, + # TODO: 公証実装したら書く. + # auto_generated: true, + canonical_id: nil) + end + end end diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index 202610b..10d6bb5 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -56,6 +56,7 @@ class Tag < ApplicationRecord has_many :tag_versions has_many :nico_tag_versions + has_many :tag_names belongs_to :tag_name delegate :wiki_page, to: :tag_name @@ -105,18 +106,20 @@ 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.tagme = find_or_create_by_tag_name!(Locale.nipponese, 'タグ希望', category: :meta) + def self.bot = find_or_create_by_tag_name!(Locale.nipponese, 'bot操作', category: :meta) + def self.no_deerjikist = + find_or_create_by_tag_name!(Locale.nipponese, 'ニジラー情報不詳', category: :meta) + def self.video = find_or_create_by_tag_name!(Locale.nipponese, '動画', category: :meta) + def self.niconico = find_or_create_by_tag_name!(Locale.nipponese, 'ニコニコ', category: :meta) + def self.youtube = find_or_create_by_tag_name!(Locale.nipponese, 'YouTube', category: :meta) - def self.normalise_tags! tag_names, with_tagme: true, - with_no_deerjikist: true, - deny_nico: true, - deny_deprecated: false, - with_sections: false + def self.normalise_tags! locale, tag_names, + with_tagme: true, + with_no_deerjikist: true, + deny_nico: true, + deny_deprecated: false, + with_sections: false if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') } raise NicoTagNormalisationError end @@ -145,7 +148,7 @@ class Tag < ApplicationRecord name = TagName.canonicalise(name).first - find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag| + find_or_create_by_tag_name!(locale, name, category: (cat || :general)).tap do |tag| if deny_deprecated && tag.deprecated? raise DeprecatedTagNormalisationError, [tag.name] end @@ -233,13 +236,23 @@ class Tag < ApplicationRecord [left_end_ms, right_end_ms].max end - def self.find_or_create_by_tag_name! name, category: - tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip) - tn = tn.canonical if tn.canonical_id? + def self.find_or_create_by_tag_name! locale, name, category: + language_code = locale.language_code + name = name.to_s.strip - Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t| - t.category = category + tn = TagName.find_or_create_by!(language_code:, name:) do + _1.script_code = locale.script_code + _1.primary_flg = true end + + tag = tn.tag + unless tag + tag = Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do + _1.category = category + end + end + + tag rescue ActiveRecord::RecordNotUnique retry end diff --git a/backend/app/models/tag_name.rb b/backend/app/models/tag_name.rb index de79d10..12fea7a 100644 --- a/backend/app/models/tag_name.rb +++ b/backend/app/models/tag_name.rb @@ -1,7 +1,7 @@ class TagName < ApplicationRecord include MyDiscard - has_one :tag + belongs_to :tag has_one :wiki_page belongs_to :canonical, class_name: 'TagName', optional: true @@ -23,6 +23,12 @@ class TagName < ApplicationRecord names.map { |name| tns[name]&.canonical&.name || name }.uniq end + def self.generate_name locale, tag, name + # TODO: 言語ごとの自動命名ロジック完成したら書く. + + "Tag_##{ tag.id }" + end + private def canonical_must_be_canonical diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 74bc2d4..5690ffa 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_19_030000) do +ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) 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 @@ -72,7 +72,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.index ["correct_post_id"], name: "index_gekanator_games_on_correct_post_id" t.index ["guessed_post_id"], name: "index_gekanator_games_on_guessed_post_id" t.index ["user_id"], name: "index_gekanator_games_on_user_id" - t.check_constraint "`question_count` >= 0", name: "chk_gekanator_games_question_count_nonnegative" end create_table "gekanator_question_examples", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -130,24 +129,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true end - create_table "languages", primary_key: "code", id: { type: :string, limit: 16 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.string "name", null: false - t.datetime "deprecated_at" - t.datetime "created_at", null: false - t.index ["deprecated_at"], name: "index_languages_on_deprecated_at" - end - - create_table "locales", primary_key: "code", id: { type: :string, limit: 32 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.string "language_code", limit: 16, null: false - t.string "script_code", limit: 4, null: false - t.string "name", null: false - t.datetime "deprecated_at" - t.datetime "created_at", null: false - t.index ["deprecated_at"], name: "index_locales_on_deprecated_at" - t.index ["language_code"], name: "index_locales_on_language_code" - t.index ["script_code"], name: "fk_rails_0b74ce96a8" - end - create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "material_id", null: false t.string "profile", default: "legacy_drive", null: false @@ -293,10 +274,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.datetime "created_at", null: false t.bigint "created_by_user_id" t.index ["created_at"], name: "index_nico_tag_versions_on_created_at" - t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc } - t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc } + t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at" + t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at" t.index ["tag_id", "version_no"], name: "index_nico_tag_versions_on_tag_id_and_version_no", unique: true - t.check_constraint "`version_no` > 0", name: "nico_tag_versions_version_no_positive" end create_table "post_implications", primary_key: ["post_id", "parent_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -305,14 +285,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["parent_post_id"], name: "index_post_implications_on_parent_post_id" - t.check_constraint "`post_id` <> `parent_post_id`", name: "chk_post_implications_no_self" end create_table "post_similarities", primary_key: ["post_id", "target_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "post_id", null: false t.bigint "target_post_id", null: false t.float "cos", null: false - t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos", order: { cos: :desc } + t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos" t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id" end @@ -379,8 +358,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.index ["post_id"], name: "index_post_versions_on_post_id" t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id" t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive" - t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "post_versions_event_type_valid" - t.check_constraint "`version_no` > 0", name: "post_versions_version_no_positive" t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\",\"nico\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"id\",\"version_no\",\"name\",\"category\",\"sections\"],\"additionalProperties\":false}}',`tags_json`)", name: "chk_post_versions_tags_json_schema" end @@ -399,14 +376,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.index ["url"], name: "index_posts_on_url", unique: true t.index ["video_ms", "id"], name: "idx_posts_video_ms_id" t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_posts_video_ms_positive" - t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive" - end - - create_table "scripts", primary_key: "code", id: { type: :string, limit: 4 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.string "name", null: false - t.datetime "deprecated_at" - t.datetime "created_at", null: false - t.index ["deprecated_at"], name: "index_scripts_on_deprecated_at" end create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -414,6 +383,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "theme", default: "system", null: false + t.string "display_density", default: "comfortable", null: false + t.string "font_size", default: "normal", null: false + t.integer "post_list_limit", default: 50, null: false + t.string "post_list_order", default: "created_at_desc", null: false + t.string "viewed_post_display", default: "show", null: false + t.boolean "tag_autocomplete_nico", default: true, null: false t.string "auto_fetch_title", default: "manual", null: false t.string "auto_fetch_thumbnail", default: "manual", null: false t.string "wiki_editor_mode", default: "split", null: false @@ -441,29 +416,21 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do end create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.bigint "tag_id" - t.string "language_code", limit: 16, null: false t.string "name", null: false - t.string "script_code", limit: 4, null: false - t.boolean "primary_flg", null: false t.bigint "canonical_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "discarded_at" - t.virtual "primary_tag_id", type: :bigint, as: "(case when `primary_flg` then `tag_id` else NULL end)" t.index ["canonical_id"], name: "index_tag_names_on_canonical_id" t.index ["discarded_at"], name: "index_tag_names_on_discarded_at" - t.index ["language_code", "name"], name: "index_tag_names_on_language_code_and_name", unique: true - t.index ["primary_tag_id", "language_code"], name: "index_tag_names_on_primary_tag_id_and_language_code", unique: true - t.index ["script_code"], name: "fk_rails_dd783b3d1c" - t.index ["tag_id"], name: "index_tag_names_on_tag_id" + t.index ["name"], name: "index_tag_names_on_name", unique: true end create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "tag_id", null: false t.bigint "target_tag_id", null: false t.float "cos", null: false - t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos", order: { cos: :desc } + t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos" t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id" end @@ -479,10 +446,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.datetime "created_at", null: false t.bigint "created_by_user_id" t.index ["created_at"], name: "index_tag_versions_on_created_at" - t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc } - t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc } + t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at" + t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at" t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true - t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive" end create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -498,7 +464,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.index ["discarded_at"], name: "index_tags_on_discarded_at" t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true t.check_constraint "(`deprecated_at` is null) or (`category` <> _utf8mb4'nico')", name: "chk_tags_deprecated_at_not_nico" - t.check_constraint "`version_no` > 0", name: "chk_tags_version_no_positive" end create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -657,7 +622,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at" t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id" - t.check_constraint "`version_no` > 0", name: "chk_wiki_pages_version_no_positive" end create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -702,8 +666,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do t.index ["created_by_user_id"], name: "index_wiki_versions_on_created_by_user_id" t.index ["wiki_page_id", "version_no"], name: "index_wiki_versions_on_wiki_page_id_and_version_no", unique: true t.index ["wiki_page_id"], name: "index_wiki_versions_on_wiki_page_id" - t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "wiki_versions_event_type_valid" - t.check_constraint "`version_no` > 0", name: "wiki_versions_version_no_positive" end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" @@ -720,8 +682,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do add_foreign_key "gekanator_question_suggestions", "users" add_foreign_key "gekanator_questions", "gekanator_question_suggestions" add_foreign_key "gekanator_questions", "users", column: "created_by_id" - add_foreign_key "locales", "languages", column: "language_code", primary_key: "code" - add_foreign_key "locales", "scripts", column: "script_code", primary_key: "code" add_foreign_key "material_export_items", "materials" add_foreign_key "material_export_items", "users", column: "created_by_user_id" add_foreign_key "material_import_blocks", "users", column: "created_by_user_id" @@ -757,10 +717,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_19_030000) do add_foreign_key "settings", "users" add_foreign_key "tag_implications", "tags" add_foreign_key "tag_implications", "tags", column: "parent_tag_id" - add_foreign_key "tag_names", "languages", column: "language_code", primary_key: "code" - add_foreign_key "tag_names", "scripts", column: "script_code", primary_key: "code" add_foreign_key "tag_names", "tag_names", column: "canonical_id" - add_foreign_key "tag_names", "tags" add_foreign_key "tag_similarities", "tags" add_foreign_key "tag_similarities", "tags", column: "target_tag_id" add_foreign_key "tag_versions", "tags"