From 1b1130ccd900aa9313f10a0146c42548910f058b Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 22 Sep 2026 02:56:58 +0900 Subject: [PATCH] =?UTF-8?q?#55=20=E3=81=84=E3=81=A3=E3=81=9F=E3=82=93?= =?UTF-8?q?=E4=B8=AD=E6=96=AD=EF=BC=9B=E5=A4=96=E9=83=A8=E3=82=BF=E3=82=B0?= =?UTF-8?q?=E5=88=86=E9=9B=A2=E5=84=AA=E5=85=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/controllers/application_controller.rb | 2 + .../app/controllers/materials_controller.rb | 7 +- .../app/controllers/nico_tags_controller.rb | 7 +- backend/app/controllers/posts_controller.rb | 7 +- backend/app/controllers/tags_controller.rb | 13 ++-- backend/app/models/tag.rb | 10 +-- backend/app/models/tag_name.rb | 2 +- backend/app/services/post_creator.rb | 18 ++--- backend/db/schema.rb | 72 +++++++++++++++---- backend/lib/tasks/sync_nico.rake | 2 +- 10 files changed, 98 insertions(+), 42 deletions(-) diff --git a/backend/app/controllers/application_controller.rb b/backend/app/controllers/application_controller.rb index 95ec17a..0023bd5 100644 --- a/backend/app/controllers/application_controller.rb +++ b/backend/app/controllers/application_controller.rb @@ -92,4 +92,6 @@ class ApplicationController < ActionController::API value end + + def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese end diff --git a/backend/app/controllers/materials_controller.rb b/backend/app/controllers/materials_controller.rb index be1353a..8bd5dae 100644 --- a/backend/app/controllers/materials_controller.rb +++ b/backend/app/controllers/materials_controller.rb @@ -72,6 +72,7 @@ class MaterialsController < ApplicationController 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,7 +90,7 @@ 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) @@ -237,9 +238,7 @@ class MaterialsController < ApplicationController end def resolve_material_tag! locale, tag_name_raw - tag_name = TagName.find_or_create_by!(language_code: locale.language_code, - name: tag_name_raw) - tag_name.tag || Tag.create!(tag_name:, category: :material) + Tag.find_or_create_by_tag_name!(locale, tag_name_raw, category: :material) end def material_index_needs_tag_name? filters diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb index ce63840..80ad31d 100644 --- a/backend/app/controllers/nico_tags_controller.rb +++ b/backend/app/controllers/nico_tags_controller.rb @@ -70,6 +70,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 = Tag.find(id) @@ -79,8 +81,9 @@ 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) if linked_tags.any? { |t| t.nico? } raise Tag::NicoTagNormalisationError end diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index e600f1a..3300d2a 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -690,9 +690,10 @@ class PostsController < ApplicationController 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!(locale, tag_names, with_tagme: false, deny_deprecated: true, - with_sections: true) => - { tags:, sections: } + 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?) video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:) diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index f152d19..4ec260b 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -278,6 +278,8 @@ class TagsController < ApplicationController return unless validate_tag_rename(tag, name) + locale = resolve_locale! + alias_names = params[:aliases].to_s.split.uniq parent_names = params[:parent_tags].to_s.split.uniq deprecated = bool?(:deprecated) @@ -308,7 +310,7 @@ class TagsController < ApplicationController alias_names.delete(name) update_aliases!(tag, alias_names) - update_parent_tags!(tag, parent_names) + update_parent_tags!(locale, tag, parent_names) tag.reload @@ -659,10 +661,11 @@ class TagsController < ApplicationController end end - def update_parent_tags! tag, parent_names - parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false, - with_no_deerjikist: false, - deny_nico: true) + def update_parent_tags! locale, tag, parent_names + parent_tags = Tag.normalise_tags!(locale, parent_names, + with_tagme: false, + with_no_deerjikist: false, + deny_nico: true) old_parent_tags = tag.parents.to_a diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index 9c80f71..6fdbccd 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -242,12 +242,14 @@ class Tag < ApplicationRecord end tag = tn.tag - unless tag - tag = Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do - _1.category = category - end + if tag + tag.update!(tag_name_id: tn.id) if tag.tag_name_id != tn.id + return tag end + tag = Tag.create!(tag_name: tn, category:) + tn.update!(tag:) + tag rescue ActiveRecord::RecordNotUnique retry diff --git a/backend/app/models/tag_name.rb b/backend/app/models/tag_name.rb index a70aa4b..82be505 100644 --- a/backend/app/models/tag_name.rb +++ b/backend/app/models/tag_name.rb @@ -1,5 +1,5 @@ class TagName < ApplicationRecord - belongs_to :tag + belongs_to :tag, optional: true has_one :wiki_page belongs_to :canonical, class_name: 'TagName', optional: true diff --git a/backend/app/services/post_creator.rb b/backend/app/services/post_creator.rb index 788a671..fd834b1 100644 --- a/backend/app/services/post_creator.rb +++ b/backend/app/services/post_creator.rb @@ -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], @@ -25,7 +25,7 @@ class PostCreator post_tags = planned_post_tags sections = planned_sections TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor) - post.video_ms = planned_video_ms + post.video_ms = planned_video_ms(locale) post.save! sync_post_tags!(post, post_tags, sections) sync_parent_posts!(post, planned_parent_post_ids) @@ -53,15 +53,15 @@ class PostCreator def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids] - def planned_video_ms - planned_create_attributes[:video_ms] + def planned_video_ms locale + planned_create_attributes(locale)[:video_ms] end - def planned_create_attributes + 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, @@ -92,13 +92,13 @@ class PostCreator 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 diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 58c768b..7b68f5f 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -72,6 +72,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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| @@ -129,6 +130,24 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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 @@ -274,9 +293,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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" - t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_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 ["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| @@ -285,13 +305,14 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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" + t.index ["post_id", "cos"], name: "index_post_similarities_on_post_id_and_cos", order: { cos: :desc } t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id" end @@ -347,6 +368,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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 @@ -365,6 +388,14 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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| @@ -372,12 +403,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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 @@ -405,19 +430,27 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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.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 ["name"], name: "index_tag_names_on_name", unique: true + 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" 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" + t.index ["tag_id", "cos"], name: "index_tag_similarities_on_tag_id_and_cos", order: { cos: :desc } t.index ["target_tag_id"], name: "index_tag_similarities_on_target_tag_id" end @@ -433,9 +466,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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" - t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_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 ["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| @@ -449,6 +483,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do t.index ["deprecated_at"], name: "index_tags_on_deprecated_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| @@ -615,11 +650,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "discarded_at" + t.integer "next_asset_no", default: 1, null: false t.integer "version_no", null: false t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id" t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at" t.index ["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| @@ -664,6 +701,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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" @@ -680,6 +719,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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" @@ -711,7 +752,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_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", "users", column: "created_by_user_id" @@ -740,6 +784,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_030000) do add_foreign_key "user_post_views", "posts" add_foreign_key "user_post_views", "users" add_foreign_key "user_theme_slots", "users" + add_foreign_key "wiki_assets", "users", column: "created_by_user_id" + add_foreign_key "wiki_assets", "wiki_pages" add_foreign_key "wiki_pages", "tag_names" add_foreign_key "wiki_pages", "users", column: "created_user_id" add_foreign_key "wiki_pages", "users", column: "updated_user_id" diff --git a/backend/lib/tasks/sync_nico.rake b/backend/lib/tasks/sync_nico.rake index f95fba1..dac9e25 100644 --- a/backend/lib/tasks/sync_nico.rake +++ b/backend/lib/tasks/sync_nico.rake @@ -124,7 +124,7 @@ namespace :nico do datum['tags'].each do |raw| name = TagNameSanitisationRule.sanitise("nico:#{ raw }") - tag = Tag.find_or_create_by_tag_name!(name, category: :nico) + tag = Tag.find_or_create_by_tag_name!(Locale.nipponese, name, category: :nico) event_type = tag.nico_tag_versions.exists? ? :update : :create NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil)