From 6ba87beef6406799792ccfc6094789767a5f9d50 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Thu, 15 Oct 2026 23:49:38 +0900 Subject: [PATCH] #431 --- .../tag_descriptions_controller.rb | 19 ++- .../app/controllers/tag_groups_controller.rb | 16 ++- .../app/controllers/wiki_pages_controller.rb | 127 ++++++++++-------- backend/app/representations/tag_group_repr.rb | 53 +++++--- backend/app/representations/wiki_page_repr.rb | 7 +- .../app/services/tag_description_operation.rb | 16 +-- .../app/services/tag_group_markdown_parser.rb | 43 +++--- backend/app/services/tag_group_operation.rb | 29 ++-- 8 files changed, 176 insertions(+), 134 deletions(-) diff --git a/backend/app/controllers/tag_descriptions_controller.rb b/backend/app/controllers/tag_descriptions_controller.rb index 06d78c2..dfbaa4f 100644 --- a/backend/app/controllers/tag_descriptions_controller.rb +++ b/backend/app/controllers/tag_descriptions_controller.rb @@ -1,14 +1,18 @@ class TagDescriptionsController < ApplicationController def show - description = TagDescription.find_by(tag_id: params[:id], language_code: 'ja') + locale = resolve_locale! + description = TagDescription.find_by(tag_id: params[:id], language_code: locale.language_code) return head :not_found unless description render json: representation(description) end def show_by_title - tag_name = TagName.find_by(language_code: 'ja', name: params[:name].to_s.strip) - description = tag_name&.tag && TagDescription.find_by(tag: tag_name.tag, language_code: 'ja') + locale = resolve_locale! + tag_name = TagName.find_by( + language_code: locale.language_code, name: params[:name].to_s.strip) + description = tag_name&.tag && TagDescription.find_by( + tag: tag_name.tag, language_code: locale.language_code) return head :not_found unless description render json: representation(description) @@ -21,18 +25,19 @@ class TagDescriptionsController < ApplicationController body = params[:body].to_s return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank? + locale = resolve_locale! tag = Tag.find_by(id: params[:id]) return head :not_found unless tag - description = TagDescription.find_by(tag:, language_code: 'ja') + description = TagDescription.find_by(tag:, language_code: locale.language_code) if description - TagDescriptionOperation.update!(tag:, body:, updated_by_user: current_user) + TagDescriptionOperation.update!(locale, tag:, body:, updated_by_user: current_user) description.reload render json: representation(description) else - TagDescriptionOperation.create!(tag:, body:, created_by_user: current_user) - description = TagDescription.find_by!(tag:, language_code: 'ja') + TagDescriptionOperation.create!(locale, tag:, body:, created_by_user: current_user) + description = TagDescription.find_by!(tag:, language_code: locale.language_code) render json: representation(description), status: :created end rescue ActiveRecord::RecordInvalid => e diff --git a/backend/app/controllers/tag_groups_controller.rb b/backend/app/controllers/tag_groups_controller.rb index 8117eea..1872f40 100644 --- a/backend/app/controllers/tag_groups_controller.rb +++ b/backend/app/controllers/tag_groups_controller.rb @@ -1,33 +1,37 @@ class TagGroupsController < ApplicationController def index + locale = resolve_locale! groups = TagGroup.includes(:created_by_user, :updated_by_user).order(:id) - render json: TagGroupRepr.many(groups.to_a) + render json: TagGroupRepr.many(locale, groups.to_a) end def show + locale = resolve_locale! group = TagGroup.includes(:created_by_user, :updated_by_user).find_by(id: params[:id]) return head :not_found unless group - render json: TagGroupRepr.one(group) + render json: TagGroupRepr.one(locale, group) end def create return head :unauthorized unless current_user return head :forbidden unless current_user.gte_member? - group = TagGroupOperation.create!(**write_attributes, actor: current_user) - render json: TagGroupRepr.one(group), status: :created + locale = resolve_locale! + group = TagGroupOperation.create!(locale, **write_attributes, actor: current_user) + render json: TagGroupRepr.one(locale, group), status: :created end def update return head :unauthorized unless current_user return head :forbidden unless current_user.gte_member? + locale = resolve_locale! group = TagGroup.find_by(id: params[:id]) return head :not_found unless group - TagGroupOperation.update!(group:, **write_attributes, actor: current_user) - render json: TagGroupRepr.one(group.reload) + TagGroupOperation.update!(locale, group:, **write_attributes, actor: current_user) + render json: TagGroupRepr.one(locale, group.reload) end private diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index 5cae0b5..a9f8953 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -4,9 +4,12 @@ class WikiPagesController < ApplicationController rescue_from TagDescriptionOperation::Conflict, with: :render_wiki_conflict def index + locale = resolve_locale! + title = params[:title].to_s.strip descriptions = - TagDescription.where(language_code: 'ja') + TagDescription + .where(language_code: locale.language_code) .includes(tag: :tag_names) descriptions = descriptions.order(updated_at: :desc, id: :asc) @@ -15,13 +18,14 @@ class WikiPagesController < ApplicationController pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%" descriptions = descriptions.joins(tag: :tag_names) - .where(tag_names: { language_code: 'ja', primary_flg: true }) + .where(tag_names: { language_code: locale.language_code, primary_flg: true }) .where('tag_names.name LIKE ?', pattern).limit(20) tag_groups = tag_groups.where( - 'CONCAT(?, name) LIKE ?', TagGroupRepr::PREFIX, pattern).limit(20) + 'CONCAT(?, name) LIKE ? OR CONCAT(?, name) LIKE ?', + TagGroupRepr::PREFIX, pattern, 'tag_group:', pattern).limit(20) end - json = descriptions.map { |description| WikiPageRepr.tag_description(description) } + json = descriptions.map { |description| WikiPageRepr.tag_description(locale, description) } json.concat(tag_groups.map { WikiPageRepr.tag_group(_1) }) json.sort_by! do |row| [-row['updated_at'].to_f, row.fetch('resource_type'), row.fetch('id')] @@ -31,47 +35,53 @@ class WikiPagesController < ApplicationController end def show - tag_description = TagDescription.find_by(id: params[:id]) + locale = resolve_locale! + tag_description = TagDescription.find_by(id: params[:id], language_code: locale.language_code) - return render_wiki_page_by_tag_description tag_description if tag_description + return render_wiki_page_by_tag_description locale, tag_description if tag_description tag_group = TagGroup.find_by(id: params[:id]) - return render_tag_group tag_group if tag_group + return render_tag_group locale, tag_group if tag_group head :not_found end def show_by_title + locale = resolve_locale! title = params[:title].to_s.strip - if title.start_with?(TagGroupRepr::PREFIX) - name = title.delete_prefix(TagGroupRepr::PREFIX) + if (name = TagGroupRepr.name_from_title(title)) tag_group = TagGroup.find_by(name:) - return render_tag_group tag_group if tag_group + return render_tag_group locale, tag_group if tag_group return head :not_found end - tag_description = tag_description_by_title(title) + tag_description = tag_description_by_title(locale, title) - return render_wiki_page_by_tag_description tag_description if tag_description + return render_wiki_page_by_tag_description locale, tag_description if tag_description head :not_found end def exists - return head :no_content if TagDescription.exists?(id: params[:id]) + locale = resolve_locale! + if TagDescription.exists?(id: params[:id], language_code: locale.language_code) + return head :no_content + end return head :no_content if TagGroup.exists?(id: params[:id]) head :not_found end def exists_by_title + locale = resolve_locale! title = params[:title].to_s.strip - if title.start_with?(TagGroupRepr::PREFIX) - exists = TagGroup.exists?(name: title.delete_prefix(TagGroupRepr::PREFIX)) + if (name = TagGroupRepr.name_from_title(title)) + exists = TagGroup.exists?(name:) return head exists ? :no_content : :not_found end - return head :no_content if tag_description_by_title(title) + + return head :no_content if tag_description_by_title(locale, title) head :not_found end @@ -80,15 +90,17 @@ class WikiPagesController < ApplicationController id = params[:id] return render_bad_request('id は必須です.') if id.blank? + locale = resolve_locale! + from = params[:from].presence to = params[:to].presence - description = TagDescription.find_by(id:) + description = TagDescription.find_by(id:, language_code: locale.language_code) if description versions = description_versions(description) from_rev = from && versions.find(from) to_rev = to ? versions.find(to) : versions.find_by!(version_no: description.version_no) - resource = WikiPageRepr.tag_description(description) + resource = WikiPageRepr.tag_description(locale, description) else tag_group = TagGroup.find_by(id:) if tag_group @@ -96,8 +108,8 @@ class WikiPagesController < ApplicationController to_rev = to ? group_version(tag_group, to) : current_group_version(tag_group) return head :not_found if (from && !from_rev) || !to_rev - from_body = from_rev && render_group_version(from_rev) - to_body = render_group_version(to_rev) + from_body = from_rev && render_group_version(locale, from_rev) + to_body = render_group_version(locale, to_rev) resource = { 'id' => tag_group.id, 'title' => "#{ TagGroupRepr::PREFIX }#{ tag_group.name }", 'deprecated_at' => nil } @@ -131,14 +143,15 @@ class WikiPagesController < ApplicationController return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank? return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank? - if title.start_with?(TagGroupRepr::PREFIX) - name = title.delete_prefix(TagGroupRepr::PREFIX) - parsed = TagGroupMarkdownParser.parse(body:, group: nil) + locale = resolve_locale! + + if (name = TagGroupRepr.name_from_title(title)) + parsed = TagGroupMarkdownParser.parse(locale, body:, group: nil) group = TagGroupOperation.create!( - name:, **parsed.except(:compatibility_body), actor: current_user, + locale, name:, **parsed.except(:compatibility_body), actor: current_user, require_compatibility: true) version = current_group_version(group) - return render_group_representation(TagGroupRepr.one(group), version, + return render_group_representation(TagGroupRepr.one(locale, group), version, status: :created) end @@ -146,14 +159,15 @@ class WikiPagesController < ApplicationController if title.include?(':') return render_unprocessable_entity field: :title else - tag_name = TagName.find_by(language_code: 'ja', name: title) + tag_name = TagName.find_by(language_code: locale.language_code, name: title) return render_unprocessable_entity field: :title unless tag_name&.tag TagDescriptionOperation.create!( + locale, tag: tag_name.tag, body:, created_by_user: current_user, message:) end - render json: WikiPageRepr.tag_description(description), status: :created + render json: WikiPageRepr.tag_description(locale, description), status: :created rescue ActiveRecord::RecordInvalid => e render_validation_error e.record rescue ActiveRecord::RecordNotUnique @@ -172,26 +186,28 @@ class WikiPagesController < ApplicationController return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank? return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank? + locale = resolve_locale! + group = TagGroup.find_by(id: params[:id]) - if title.start_with?(TagGroupRepr::PREFIX) + if (name = TagGroupRepr.name_from_title(title)) return head :not_found unless group - name = title.delete_prefix(TagGroupRepr::PREFIX) - parsed = TagGroupMarkdownParser.parse(body:, group:) + parsed = TagGroupMarkdownParser.parse(locale, body:, group:) TagGroupOperation.update!( - group:, name:, **parsed.except(:compatibility_body), actor: current_user, + locale, group:, name:, **parsed.except(:compatibility_body), actor: current_user, require_compatibility: true) return head :ok end return render_unprocessable_entity field: :title if title.include?(':') - description = TagDescription.find_by(id: params[:id]) + description = TagDescription.find_by(id: params[:id], language_code: locale.language_code) unless description return render_unprocessable_entity field: :title if group return head :not_found end + tag = description.tag - tag_name = tag.tag_name('ja') + tag_name = tag.tag_name(locale.language_code) return render_unprocessable_entity field: :title unless tag_name return render_unprocessable_entity field: :title if tag_name.name.include?(':') @@ -207,7 +223,7 @@ class WikiPagesController < ApplicationController tag_name.update!(name: title) if title != old_title message = params[:message].presence - TagDescriptionOperation.update!(tag:, body:, updated_by_user: current_user, + TagDescriptionOperation.update!(locale, tag:, body:, updated_by_user: current_user, message:, base_revision_id:) if title != old_title @@ -226,10 +242,12 @@ class WikiPagesController < ApplicationController end def changes + locale = resolve_locale! + id = params[:id].presence - description = id && TagDescription.find_by(id:) + description = id && TagDescription.find_by(id:, language_code: locale.language_code) if description - return render json: description_changes(TagDescription.where(id: description.id)) + return render json: description_changes(locale, TagDescription.where(id: description.id)) end tag_group = id && TagGroup.find_by(id:) @@ -240,7 +258,8 @@ class WikiPagesController < ApplicationController json = [] unless id json.concat(tag_group_changes(TagGroupVersion.all)) - json.concat(description_changes(TagDescription.where(language_code: 'ja'))) + json.concat(description_changes(locale, + TagDescription.where(language_code: locale.language_code))) json = json.each_with_index.sort_by do |row, index| [-row.fetch(:timestamp).to_f, index] end.map(&:first) @@ -250,7 +269,7 @@ class WikiPagesController < ApplicationController private - def render_tag_group group + def render_tag_group locale, group version = if params[:version].present? group_version(group, params[:version]) else @@ -258,16 +277,17 @@ class WikiPagesController < ApplicationController end return head :not_found unless version - representation = if params[:version].present? - TagGroupRepr.historical(version) - else - TagGroupRepr.one(group) - end + representation = + if params[:version].present? + TagGroupRepr.historical(locale, version) + else + TagGroupRepr.one(locale, group) + end render_group_representation(representation, version) end - def render_group_version version - representation = TagGroupRepr.historical(version) + def render_group_version locale, version + representation = TagGroupRepr.historical(locale, version) TagGroupMarkdownRenderer.render(representation) end @@ -336,7 +356,7 @@ class WikiPagesController < ApplicationController end end - def render_wiki_page_by_tag_description description + def render_wiki_page_by_tag_description locale, description versions = description_versions(description) historical = params[:version].present? rev = historical ? versions.find_by(id: params[:version]) : @@ -347,7 +367,7 @@ class WikiPagesController < ApplicationController succ = versions.find_by(version_no: rev.version_no + 1)&.id body = historical ? rev.body : description.body updated_at = historical ? rev.created_at : description.updated_at - render json: WikiPageRepr.tag_description(description).merge( + render json: WikiPageRepr.tag_description(locale, description).merge( body:, revision_id: rev.id, pred:, succ:, updated_at:) end @@ -356,13 +376,14 @@ class WikiPagesController < ApplicationController language_code: description.language_code) end - def tag_description_by_title title + def tag_description_by_title locale, title TagDescription.joins(tag: :tag_names) - .find_by(language_code: 'ja', tag_names: { language_code: 'ja', name: title }) + .find_by(language_code: locale.language_code, + tag_names: { language_code: locale.language_code, name: title }) end - def description_changes descriptions - versions = TagDescriptionVersion.where(language_code: 'ja', + def description_changes locale, descriptions + versions = TagDescriptionVersion.where(language_code: locale.language_code, tag_id: descriptions.select(:tag_id)) .order(id: :desc).limit(200).to_a return [] if versions.empty? @@ -371,7 +392,7 @@ class WikiPagesController < ApplicationController .includes(tag: :tag_names).index_by(&:tag_id) users = User.where(id: versions.map(&:created_by_user_id)).index_by(&:id) predecessors = TagDescriptionVersion.where( - language_code: 'ja', tag_id: by_tag.keys, + language_code: locale.language_code, tag_id: by_tag.keys, version_no: versions.map { |version| version.version_no - 1 }) .pluck(:tag_id, :version_no, :id) .to_h { |tag_id, version_no, id| [[tag_id, version_no], id] } @@ -381,7 +402,7 @@ class WikiPagesController < ApplicationController { revision_id: version.id, pred: predecessors[[version.tag_id, version.version_no - 1]], succ: nil, - wiki_page: { id: description.id, title: description.tag.name('ja'), + wiki_page: { id: description.id, title: description.tag.name(locale.language_code), deprecated_at: description.tag.deprecated_at }, user: user && { id: user.id, name: user.name }, kind: 'content', diff --git a/backend/app/representations/tag_group_repr.rb b/backend/app/representations/tag_group_repr.rb index 98a5cb4..1b2afa4 100644 --- a/backend/app/representations/tag_group_repr.rb +++ b/backend/app/representations/tag_group_repr.rb @@ -3,7 +3,12 @@ module TagGroupRepr module_function - def many groups + def name_from_title title + prefix = [PREFIX, 'tag_group:'].find { title.downcase.start_with?(_1) } + title[prefix.length..] if prefix + end + + def many locale, groups ids = groups.map(&:id) return [] if ids.empty? @@ -14,14 +19,20 @@ module TagGroupRepr placements = TagGroupPlacement.where(tag_group_id: ids).order(:position) .group_by(&:tag_group_id) - target_ids = tag_members.values.flatten.map(&:tag_id) + - group_members.values.flatten.map(&:child_tag_group_id) + + target_ids = + tag_members.values.flatten.map(&:tag_id) + placements.values.flatten.filter_map(&:tag_id) - child_ids = group_members.values.flatten.map(&:child_tag_group_id) + + child_ids = + group_members.values.flatten.map(&:child_tag_group_id) + placements.values.flatten.filter_map(&:child_tag_group_id) tags = Tag.select(:id, :category).where(id: target_ids.uniq).index_by(&:id) - names = TagName.where(tag_id: target_ids.uniq, language_code: 'ja', primary_flg: true) - .pluck(:tag_id, :name).to_h + names = + TagName + .where(tag_id: target_ids.uniq, + language_code: locale.language_code, + primary_flg: true) + .pluck(:tag_id, :name) + .to_h child_groups = TagGroup.where(id: child_ids.uniq).index_by(&:id) groups.map do |group| @@ -32,8 +43,8 @@ module TagGroupRepr end end - def one group - many([group]).first + def one locale, group + many(locale, [group]).first end def tag_group wiki_page @@ -48,7 +59,7 @@ module TagGroupRepr TagGroupVersion.find_by(tag_group_id: group.id, version_no: group.version_no) end - def historical version + def historical locale, version members = Array(version.members) placements = Array(version.placements) tag_references = members.filter_map { |member| member['tag_id'] } + @@ -68,10 +79,10 @@ module TagGroupRepr [member.fetch('tag_group_id'), member.fetch('version_no')] end.to_h historical_members = members.map do |member| - historical_member(member, tag_versions, group_versions) + historical_member(locale, member, tag_versions, group_versions) end historical_placements = placements.map do |node| - historical_placement(node, tag_versions, group_versions, + historical_placement(locale, node, tag_versions, group_versions, tag_version_numbers, group_version_numbers) end { 'id' => version.tag_group_id, @@ -89,7 +100,7 @@ module TagGroupRepr def group_json group, tag_members, group_members, rows, tags, names, child_groups tree = placement_tree(rows, tags, names, child_groups) members = tag_members.sort_by(&:tag_id).map do |member| - { 'tag' => tag_json(tags.fetch(member.tag_id), names.fetch(member.tag_id)) } + { 'tag' => tag_json(tags.fetch(member.tag_id), names[member.tag_id]) } end members.concat(group_members.sort_by(&:child_tag_group_id).map do |member| { 'tag_group' => group_json_reference(child_groups.fetch(member.child_tag_group_id)) } @@ -115,7 +126,7 @@ module TagGroupRepr build = lambda do |parent| by_parent.fetch(parent, []).sort_by(&:position).map do |row| node = if row.tag_id - { 'tag' => tag_json(tags.fetch(row.tag_id), names.fetch(row.tag_id)) } + { 'tag' => tag_json(tags.fetch(row.tag_id), names[row.tag_id]) } elsif row.child_tag_group_id { 'tag_group' => group_json_reference(child_groups.fetch(row.child_tag_group_id)) } else @@ -153,11 +164,11 @@ module TagGroupRepr end end - def historical_member member, tag_versions, group_versions + def historical_member locale, member, tag_versions, group_versions if member['tag_id'] id = member.fetch('tag_id') version = tag_versions.fetch([id, member.fetch('version_no')]) - { 'tag' => { 'id' => id, 'name' => japanese_version_name(version), + { 'tag' => { 'id' => id, 'name' => version_name(locale, version), 'category' => version.category } } else id = member.fetch('tag_group_id') @@ -166,13 +177,13 @@ module TagGroupRepr end end - def historical_placement(node, tag_versions, group_versions, - tag_version_numbers, group_version_numbers) + def historical_placement locale, node, tag_versions, group_versions, + tag_version_numbers, group_version_numbers result = if node['tag_id'] id = node.fetch('tag_id') version_no = tag_version_numbers.fetch(id) version = tag_versions.fetch([id, version_no]) - { 'tag' => { 'id' => id, 'name' => japanese_version_name(version), + { 'tag' => { 'id' => id, 'name' => version_name(locale, version), 'category' => version.category } } elsif node['tag_group_id'] id = node.fetch('tag_group_id') @@ -183,12 +194,12 @@ module TagGroupRepr { 'section_name' => node.fetch('section_name'), 'description' => node['description'] } end result.merge('children' => Array(node['children']).map do |child| - historical_placement(child, tag_versions, group_versions, + historical_placement(locale, child, tag_versions, group_versions, tag_version_numbers, group_version_numbers) end) end - def japanese_version_name version - version.names.fetch('ja').fetch('name') + def version_name locale, version + version.names[locale.language_code]&.fetch('name') end end diff --git a/backend/app/representations/wiki_page_repr.rb b/backend/app/representations/wiki_page_repr.rb index 2c747ba..74324d8 100644 --- a/backend/app/representations/wiki_page_repr.rb +++ b/backend/app/representations/wiki_page_repr.rb @@ -4,11 +4,12 @@ module WikiPageRepr module_function - def tag_description description + def tag_description locale, description tag = description.tag + { 'id' => description.id, 'resource_type' => 'tag_description', - 'tag_name_id' => tag.tag_name('ja')&.id, + 'tag_name_id' => tag.tag_name(locale.language_code)&.id, 'body' => description.body, 'created_user_id' => description.created_by_user_id, 'updated_user_id' => description.updated_by_user_id, @@ -16,7 +17,7 @@ module WikiPageRepr 'updated_at' => description.updated_at, 'discarded_at' => nil, 'version_no' => description.version_no, - 'title' => tag.name('ja'), + 'title' => tag.name(locale.language_code), 'deprecated_at' => tag.deprecated_at } end diff --git a/backend/app/services/tag_description_operation.rb b/backend/app/services/tag_description_operation.rb index 80284e8..991390c 100644 --- a/backend/app/services/tag_description_operation.rb +++ b/backend/app/services/tag_description_operation.rb @@ -1,32 +1,30 @@ class TagDescriptionOperation - LANGUAGE_CODE = 'ja' - class Conflict < StandardError ; end - def self.create!(tag:, body:, created_by_user:, message: nil) + def self.create! locale, tag:, body:, created_by_user:, message: nil ApplicationRecord.transaction do tag.lock! raise ActiveRecord::RecordNotUnique if TagDescription.exists?( - tag_id: tag.id, language_code: LANGUAGE_CODE) + tag_id: tag.id, language_code: locale.language_code) - description = TagDescription.new(tag:, language_code: LANGUAGE_CODE) + description = TagDescription.new(tag:, language_code: locale.language_code) body = normalise_body!(description, body) record_version!(description:, body:, user: created_by_user) + description end end - def self.update!(tag:, body:, updated_by_user:, message: nil, - base_revision_id: nil) + def self.update! locale, tag:, body:, updated_by_user:, message: nil, base_revision_id: nil ApplicationRecord.transaction do tag.lock! description = TagDescription.lock.find_by!( - tag_id: tag.id, language_code: LANGUAGE_CODE) + tag_id: tag.id, language_code: locale.language_code) body = normalise_body!(description, body) latest = TagDescriptionVersion.find_by( - tag_id: tag.id, language_code: LANGUAGE_CODE, + tag_id: tag.id, language_code: locale.language_code, version_no: description.version_no) if base_revision_id.present? && latest&.id != base_revision_id.to_i raise Conflict, '競合が発生してゐます.' diff --git a/backend/app/services/tag_group_markdown_parser.rb b/backend/app/services/tag_group_markdown_parser.rb index bfbeba7..595e7b1 100644 --- a/backend/app/services/tag_group_markdown_parser.rb +++ b/backend/app/services/tag_group_markdown_parser.rb @@ -7,8 +7,8 @@ class TagGroupMarkdownParser ; end - def self.parse(body:, group: nil) - new(body:, group:).parse + def self.parse(locale, body:, group: nil) + new(body:, group:).parse(locale) end def initialize(body:, group:) @@ -16,7 +16,7 @@ class TagGroupMarkdownParser @group = group end - def parse + def parse locale parse_body = @body.gsub(/([^\n])\n([\x23]{1,6}[ \t]+\S)/, "\\1\n\n\\2") root = Kramdown::Document.new(parse_body, input: 'GFM', auto_ids: false).root placements = [] @@ -51,7 +51,7 @@ class TagGroupMarkdownParser when :ul invalid!('未対応のリスト項目があります.') if node.attr.any? target = headings.any? ? headings.last.fetch(:node).fetch('children') : placements - target.concat(node.children.map { parse_list_item(_1) }) + target.concat(node.children.map { parse_list_item(locale, _1) }) else invalid!("未対応の Markdown 構文 #{ node.type.inspect } です.") end @@ -66,7 +66,7 @@ class TagGroupMarkdownParser private - def parse_list_item(item) + def parse_list_item locale, item invalid!('リスト項目ではありません.') unless item.type == :li invalid!('未対応のリスト項目があります.') if item.attr.any? @@ -81,29 +81,30 @@ class TagGroupMarkdownParser label = own.map { inline_text(_1) }.join(' ').strip invalid!('空のリスト項目は使用できません.') if label.blank? - node = if label.start_with?(PREFIX) - group_name = label.delete_prefix(PREFIX).strip - child_group = TagGroup.find_by(name: group_name) - invalid!("Tag Group #{ group_name.inspect } を解決できません.") unless child_group - invalid!('自分自身を含めることはできません.') if child_group.id == @group&.id - { 'tag_group_id' => child_group.id } - elsif label.end_with?(':', ':') - section_name = label.sub(/[::]\z/, '').strip - invalid!('Section 名は必須です.') if section_name.blank? - { 'section_name' => section_name, 'description' => nil } - else - { 'tag_id' => resolve_tag(label) } - end + node = + if (group_name = TagGroupRepr.name_from_title(label)) + group_name = group_name.strip + child_group = TagGroup.find_by(name: group_name) + invalid!("Tag Group #{ group_name.inspect } を解決できません.") unless child_group + invalid!('自分自身を含めることはできません.') if child_group.id == @group&.id + { 'tag_group_id' => child_group.id } + elsif label.end_with?(':', ':') + section_name = label.sub(/[::]\z/, '').strip + invalid!('Section 名は必須です.') if section_name.blank? + { 'section_name' => section_name, 'description' => nil } + else + { 'tag_id' => resolve_tag(locale, label) } + end node['children'] = nested.flat_map do |list| invalid!('未対応の入れ子リストです.') unless list.type == :ul - list.children.map { parse_list_item(_1) } + list.children.map { parse_list_item(locale, _1) } end node end - def resolve_tag(name) - names = TagName.where(language_code: 'ja', name:).to_a + def resolve_tag locale, name + names = TagName.where(language_code: locale.language_code, name:).to_a tag_ids = names.filter_map(&:tag_id).uniq invalid!("Tag #{ name.inspect } を解決できません.") if tag_ids.empty? invalid!("Tag 名 #{ name.inspect } が曖昧です.") if tag_ids.many? diff --git a/backend/app/services/tag_group_operation.rb b/backend/app/services/tag_group_operation.rb index 6cd506c..a3f653b 100644 --- a/backend/app/services/tag_group_operation.rb +++ b/backend/app/services/tag_group_operation.rb @@ -1,29 +1,29 @@ class TagGroupOperation MAX_PLACEMENT_DEPTH = 5 - def self.create!(name:, description:, members:, placements:, actor:, + def self.create!(locale, name:, description:, members:, placements:, actor:, require_compatibility: false) ApplicationRecord.transaction do group = TagGroup.new(version_no: 1, created_by_user: actor, updated_by_user: actor) - attributes = prepare!(group:, name:, description:, members:, placements:) + attributes = prepare!(locale:, group:, name:, description:, members:, placements:) group.assign_attributes(name: attributes.fetch(:name), description: attributes.fetch(:description)) group.save! persist_structure!(group:, attributes:, actor:) - validate_compatibility!(group) if require_compatibility + validate_compatibility!(locale, group) if require_compatibility persist_version!(group:, attributes:, actor:, event_type: 'create') group end end - def self.update!(group:, name:, description:, members:, placements:, actor:, + def self.update!(locale, group:, name:, description:, members:, placements:, actor:, require_compatibility: false) ApplicationRecord.transaction do group.lock! - attributes = prepare!(group:, name:, description:, members:, placements:) + attributes = prepare!(locale:, group:, name:, description:, members:, placements:) if unchanged?(group, attributes) - validate_compatibility!(group) if require_compatibility + validate_compatibility!(locale, group) if require_compatibility next group end @@ -32,20 +32,20 @@ class TagGroupOperation version_no: group.version_no + 1, updated_by_user: actor) persist_structure!(group:, attributes:, actor:) - validate_compatibility!(group) if require_compatibility + validate_compatibility!(locale, group) if require_compatibility persist_version!(group:, attributes:, actor:, event_type: 'update') group end end - def self.validate_compatibility! group - TagGroupMarkdownRenderer.render(TagGroupRepr.one(group)) + def self.validate_compatibility! locale, group + TagGroupMarkdownRenderer.render(TagGroupRepr.one(locale, group)) rescue TagGroupMarkdownRenderer::UnsupportedStructure => error invalid!(group, :base, error.message) end private_class_method :validate_compatibility! - def self.prepare!(group:, name:, description:, members:, placements:) + def self.prepare!(locale:, group:, name:, description:, members:, placements:) record = group normalized_name = name.to_s.strip invalid!(record, :name, 'グループ名は必須です.') if normalized_name.blank? @@ -63,7 +63,7 @@ class TagGroupOperation member_rows = normalize_members(record, members) placement_tree = normalize_placements(record, placements) - validate_references!(record, member_rows, placement_tree) + validate_references!(locale, record, member_rows, placement_tree) validate_containment!(record, member_rows) { name: normalized_name, description:, members: member_rows, placements: placement_tree } @@ -187,7 +187,7 @@ class TagGroupOperation end private_class_method :normalize_placement_nodes - def self.validate_references!(record, members, placements) + def self.validate_references!(locale, record, members, placements) membership = members.to_set placed = placement_references(placements).uniq missing = placed - membership.to_a @@ -206,13 +206,14 @@ class TagGroupOperation tag_versions = TagVersion.where(tag_id: tag_ids).pluck(:tag_id, :version_no).to_set group_versions = TagGroupVersion.where(tag_group_id: group_ids) .pluck(:tag_group_id, :version_no).to_set - tag_names = TagName.where(tag_id: tag_ids, language_code: 'ja', primary_flg: true) + tag_names = TagName.where( + tag_id: tag_ids, language_code: locale.language_code, primary_flg: true) .pluck(:tag_id).uniq members.each do |member| if member.key?('tag_id') id = member.fetch('tag_id') - invalid!(record, :members, "Tag ##{ id } に日本語の正規名がありません.") unless + invalid!(record, :members, :invalid) unless tag_names.include?(id) version_no = tags.fetch(id).version_no unless tag_versions.include?([id, version_no])