| @@ -30,16 +30,31 @@ class NicoTagsController < ApplicationController | |||||
| id = params[:id].to_i | id = params[:id].to_i | ||||
| tag = Tag.find(id) | tag = Tag.find(id) | ||||
| return head :bad_request if tag.category != 'nico' | |||||
| return head :bad_request unless tag.nico? | |||||
| linked_tag_names = params[:tags].to_s.split(' ') | |||||
| linked_tag_names = params[:tags].to_s.split | |||||
| linked_tags = Tag.normalise_tags(linked_tag_names, with_tagme: false, | linked_tags = Tag.normalise_tags(linked_tag_names, with_tagme: false, | ||||
| with_no_deerjikist: false) | with_no_deerjikist: false) | ||||
| return head :bad_request if linked_tags.any? { |t| t.category == 'nico' } | |||||
| return head :bad_request if linked_tags.any? { |t| t.nico? } | |||||
| tag.linked_tags = linked_tags | |||||
| tag.save! | |||||
| ApplicationRecord.transaction do | |||||
| record_tag_snapshots!(linked_tags, created_by_user: current_user) | |||||
| tag.linked_tags = linked_tags | |||||
| tag.save! | |||||
| NicoTagVersionRecorder.record!(tag:, event_type: :update, created_by_user: current_user) | |||||
| end | |||||
| render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok | render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok | ||||
| end | end | ||||
| private | |||||
| def record_tag_snapshots! tags, created_by_user: | |||||
| tags.each do |tag| | |||||
| event_type = tag.tag_versions.exists? ? :update : :create | |||||
| TagVersionRecorder.record!(tag:, event_type:, created_by_user:) | |||||
| end | |||||
| end | |||||
| end | end | ||||
| @@ -128,9 +128,11 @@ class PostsController < ApplicationController | |||||
| original_created_from:, original_created_before:) | original_created_from:, original_created_before:) | ||||
| post.thumbnail.attach(thumbnail) | post.thumbnail.attach(thumbnail) | ||||
| ActiveRecord::Base.transaction do | |||||
| ApplicationRecord.transaction do | |||||
| post.save! | post.save! | ||||
| tags = Tag.normalise_tags(tag_names) | tags = Tag.normalise_tags(tag_names) | ||||
| record_tag_snapshots!(tags, created_by_user: current_user) | |||||
| tags = Tag.expand_parent_tags(tags) | tags = Tag.expand_parent_tags(tags) | ||||
| sync_post_tags!(post, tags) | sync_post_tags!(post, tags) | ||||
| post.resized_thumbnail! | post.resized_thumbnail! | ||||
| @@ -170,10 +172,13 @@ class PostsController < ApplicationController | |||||
| post = Post.find(params[:id].to_i) | post = Post.find(params[:id].to_i) | ||||
| ActiveRecord::Base.transaction do | |||||
| ApplicationRecord.transaction do | |||||
| post.update!(title:, original_created_from:, original_created_before:) | post.update!(title:, original_created_from:, original_created_before:) | ||||
| tags = post.tags.where(category: 'nico').to_a + | |||||
| Tag.normalise_tags(tag_names, with_tagme: false) | |||||
| normalised_tag = Tag.normalise_tags(tag_names, with_tagme: false) | |||||
| record_tag_snapshots(normalised_tags, create_by_user: current_user) | |||||
| tags = post.tags.where(category: 'nico').to_a + normalised_tags | |||||
| tags = Tag.expand_parent_tags(tags) | tags = Tag.expand_parent_tags(tags) | ||||
| sync_post_tags!(post, tags) | sync_post_tags!(post, tags) | ||||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user) | PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user) | ||||
| @@ -7,7 +7,14 @@ class TagChildrenController < ApplicationController | |||||
| child_id = params[:child_id] | child_id = params[:child_id] | ||||
| return head :bad_request if parent_id.blank? || child_id.blank? | return head :bad_request if parent_id.blank? || child_id.blank? | ||||
| Tag.find(parent_id).children << Tag.find(child_id) rescue nil | |||||
| parent = Tag.find(parent_id) | |||||
| child = Tag.find(child_id) | |||||
| return head :bad_request if parent.nico? || child.nico? | |||||
| ApplicationRecord.transaction do | |||||
| TagImplication.find_or_create_by!(parent_tag: parent, tag: child) | |||||
| TagVersionRecorder.record!(tag: child, event_type: :update, created_by_user: current_user) | |||||
| end | |||||
| head :no_content | head :no_content | ||||
| end | end | ||||
| @@ -20,7 +27,14 @@ class TagChildrenController < ApplicationController | |||||
| child_id = params[:child_id] | child_id = params[:child_id] | ||||
| return head :bad_request if parent_id.blank? || child_id.blank? | return head :bad_request if parent_id.blank? || child_id.blank? | ||||
| Tag.find(parent_id).children.delete(Tag.find(child_id)) rescue nil | |||||
| parent = Tag.find(parent_id) | |||||
| child = Tag.find(child_id) | |||||
| return head :bad_request if parent.nico? || child.nico? | |||||
| ApplicationRecord.transaction do | |||||
| TagImplication.find_by(parent_tag: parent, tag: child)&.destroy! | |||||
| TagVersionRecorder.record!(tag: child, event_type: :update, created_by_user: current_user) | |||||
| end | |||||
| head :no_content | head :no_content | ||||
| end | end | ||||
| @@ -218,15 +218,25 @@ class TagsController < ApplicationController | |||||
| tag = Tag.find(params[:id]) | tag = Tag.find(params[:id]) | ||||
| if name.present? | |||||
| tag.tag_name.update!(name:) | |||||
| end | |||||
| ApplicationRecord.transaction do | |||||
| old_nico = tag.nico? | |||||
| if category.present? | |||||
| new_nico = category == 'nico' | |||||
| if old_nico != new_nico | |||||
| return render json: { error: 'ニコタグのカテゴリ変更はできません.' }, | |||||
| status: :unprocessable_entity | |||||
| end | |||||
| end | |||||
| tag.tag_name.update!(name:) if name.present? | |||||
| tag.update!(category:) if category.present? | |||||
| if category.present? | |||||
| tag.update!(category:) | |||||
| record_tag_version!(tag, event_type: :update, created_by_user: current_user) | |||||
| end | end | ||||
| render json: TagRepr.base(tag) | |||||
| render json: TagRepr.base(tag.reload) | |||||
| end | end | ||||
| private | private | ||||
| @@ -244,4 +254,12 @@ class TagsController < ApplicationController | |||||
| children: tag.children.sort_by { _1.name }.map { build_tag_children(_1) }, | children: tag.children.sort_by { _1.name }.map { build_tag_children(_1) }, | ||||
| material: material.as_json&.merge(file:, content_type:)) | material: material.as_json&.merge(file:, content_type:)) | ||||
| end | end | ||||
| def record_tag_version!(tag, event_type:, created_by_user:) | |||||
| if tag.nico? | |||||
| NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:) | |||||
| else | |||||
| TagVersionRecorder.record!(tag:, event_type:, created_by_user:) | |||||
| end | |||||
| end | |||||
| end | end | ||||
| @@ -1,7 +1,11 @@ | |||||
| module MyDiscard | module MyDiscard | ||||
| extend ActiveSupport::Concern | extend ActiveSupport::Concern | ||||
| included { include Discard::Model } | |||||
| included do | |||||
| include Discard::Model | |||||
| default_scope -> { kept } | |||||
| end | |||||
| class_methods do | class_methods do | ||||
| def find_undiscard_or_create_by! attrs, &block | def find_undiscard_or_create_by! attrs, &block | ||||
| @@ -1,29 +1,13 @@ | |||||
| class PostVersion < ApplicationRecord | class PostVersion < ApplicationRecord | ||||
| before_update do | |||||
| raise ActiveRecord::ReadOnlyRecord, '版は更新できません.' | |||||
| end | |||||
| before_destroy do | |||||
| raise ActiveRecord::ReadOnlyRecord, '版は削除できません.' | |||||
| end | |||||
| include VersionRecord | |||||
| belongs_to :post | belongs_to :post | ||||
| belongs_to :parent, class_name: 'Post', optional: true | belongs_to :parent, class_name: 'Post', optional: true | ||||
| belongs_to :created_by_user, class_name: 'User', optional: true | |||||
| enum :event_type, { create: 'create', | |||||
| update: 'update', | |||||
| discard: 'discard', | |||||
| restore: 'restore' }, prefix: true, validate: true | |||||
| validates :version_no, presence: true, numericality: { only_integer: true, greater_than: 0 } | |||||
| validates :event_type, presence: true, inclusion: { in: event_types.keys } | |||||
| validates :url, presence: true | validates :url, presence: true | ||||
| validate :validate_original_created_range | validate :validate_original_created_range | ||||
| scope :chronological, -> { order(:version_no, :id) } | |||||
| private | private | ||||
| def validate_original_created_range | def validate_original_created_range | ||||
| @@ -8,8 +8,6 @@ class Tag < ApplicationRecord | |||||
| ; | ; | ||||
| end | end | ||||
| default_scope -> { kept } | |||||
| has_many :post_tags, inverse_of: :tag | has_many :post_tags, inverse_of: :tag | ||||
| has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag | has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag | ||||
| has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag' | has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag' | ||||
| @@ -36,6 +34,9 @@ class Tag < ApplicationRecord | |||||
| has_many :deerjikists, dependent: :delete_all | has_many :deerjikists, dependent: :delete_all | ||||
| has_many :materials | has_many :materials | ||||
| has_many :tag_versions | |||||
| has_many :nico_tag_versions | |||||
| belongs_to :tag_name | belongs_to :tag_name | ||||
| delegate :wiki_page, to: :tag_name | delegate :wiki_page, to: :tag_name | ||||
| @@ -152,10 +153,11 @@ class Tag < ApplicationRecord | |||||
| retry | retry | ||||
| end | end | ||||
| def self.merge_tags! target_tag, source_tags | |||||
| def self.merge_tags! target_tag, source_tags, created_by_user: nil | |||||
| target_tag => Tag | target_tag => Tag | ||||
| affected_post_ids = Set.new | affected_post_ids = Set.new | ||||
| affected_tag_ids = Set.new | |||||
| Tag.transaction do | Tag.transaction do | ||||
| Array(source_tags).compact.uniq.each do |source_tag| | Array(source_tags).compact.uniq.each do |source_tag| | ||||
| @@ -166,7 +168,7 @@ class Tag < ApplicationRecord | |||||
| source_tag.post_tags.kept.find_each do |source_pt| | source_tag.post_tags.kept.find_each do |source_pt| | ||||
| post_id = source_pt.post_id | post_id = source_pt.post_id | ||||
| affected_post_ids << post_id | affected_post_ids << post_id | ||||
| source_pt.discard_by!(nil) | |||||
| source_pt.discard_by!(created_by_user) | |||||
| unless PostTag.kept.exists?(post_id:, tag: target_tag) | unless PostTag.kept.exists?(post_id:, tag: target_tag) | ||||
| PostTag.create!(post_id:, tag: target_tag) | PostTag.create!(post_id:, tag: target_tag) | ||||
| end | end | ||||
| @@ -179,6 +181,7 @@ class Tag < ApplicationRecord | |||||
| end | end | ||||
| source_tag.discard! | source_tag.discard! | ||||
| record_tag_discard!(source_tag, current_by_user: nil) | |||||
| if source_tag.nico? | if source_tag.nico? | ||||
| source_tag_name.discard! | source_tag_name.discard! | ||||
| @@ -186,10 +189,12 @@ class Tag < ApplicationRecord | |||||
| source_tag_name.update_columns(canonical_id: target_tag.tag_name_id, | source_tag_name.update_columns(canonical_id: target_tag.tag_name_id, | ||||
| updated_at: Time.current) | updated_at: Time.current) | ||||
| end | end | ||||
| record_tag_version!(target_tag, event_type: :update, created_by_user: nil) | |||||
| end | end | ||||
| Post.where(id: affected_post_ids.to_a).find_each do |post| | Post.where(id: affected_post_ids.to_a).find_each do |post| | ||||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil) | |||||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user:) | |||||
| end | end | ||||
| # 投稿件数を再集計 | # 投稿件数を再集計 | ||||
| @@ -199,6 +204,14 @@ class Tag < ApplicationRecord | |||||
| target_tag.reload | target_tag.reload | ||||
| end | end | ||||
| def snapshot_aliases = tag_name.aliases.kept.order(:name).pluck(:name) | |||||
| def snapshot_parent_tag_ids = parents.order('id').pluck('id') | |||||
| def snapshot_linked_tags | |||||
| linked_tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name') | |||||
| end | |||||
| private | private | ||||
| def nico_tag_name_must_start_with_nico | def nico_tag_name_must_start_with_nico | ||||
| @@ -0,0 +1,16 @@ | |||||
| class TagVersion < ApplicationRecord | |||||
| include VersionRecord | |||||
| belongs_to :tag | |||||
| enum :category, { deerjikist: 'deerjikist', | |||||
| meme: 'meme', | |||||
| character: 'character', | |||||
| general: 'general', | |||||
| material: 'material', | |||||
| meta: 'meta', | |||||
| nico: 'nico' }, validate: true | |||||
| validates :name, presence: true | |||||
| validates :category, presence: true | |||||
| end | |||||
| @@ -0,0 +1,19 @@ | |||||
| module VersionRecord | |||||
| extend ActiveSupport::Concern | |||||
| def readonly? = persisted? | |||||
| included do | |||||
| belongs_to :created_by_user, class_name: 'User', optional: true | |||||
| enum :event_type, { create: 'create', | |||||
| update: 'update', | |||||
| discard: 'discard', | |||||
| restore: 'restore' }, prefix: true, validate: true | |||||
| validates :version_no, presence: true, numericality: { only_integer: true, greater_than: 0 } | |||||
| validates :event_type, presence: true | |||||
| scope :chronological, -> { order(:version_no, :id) } | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,16 @@ | |||||
| class NicoTagVersionRecorder < VersionRecorder | |||||
| def self.record! tag:, event_type:, created_by_user: | |||||
| new(tag:, event_type:, created_by_user:).record! | |||||
| end | |||||
| def initialize tag:, event_type:, created_by_user: | |||||
| super(record: tag, event_type:, created_by_user:) | |||||
| end | |||||
| private | |||||
| def version_class = NicoTagVersion | |||||
| def version_association = :nico_tag_versions | |||||
| def record_key = :tag | |||||
| def snapshot_attributes = { name: @tag.name, linked_tags: @tag.snapshot_linked_tags.join(' ') } | |||||
| end | |||||
| @@ -4,36 +4,15 @@ class PostVersionRecorder | |||||
| end | end | ||||
| def initialize post:, event_type:, created_by_user: | def initialize post:, event_type:, created_by_user: | ||||
| @post = post | |||||
| @event_type = event_type | |||||
| @created_by_user = created_by_user | |||||
| end | |||||
| def record! | |||||
| @post.with_lock do | |||||
| latest = @post.post_versions.order(version_no: :desc).first | |||||
| attrs = snapshot_attributes | |||||
| return latest if @event_type == :update && latest && same_snapshot?(latest, attrs) | |||||
| PostVersion.create!( | |||||
| post: @post, | |||||
| version_no: (latest&.version_no || 0) + 1, | |||||
| event_type: @event_type, | |||||
| title: attrs[:title], | |||||
| url: attrs[:url], | |||||
| thumbnail_base: attrs[:thumbnail_base], | |||||
| tags: attrs[:tags], | |||||
| parent: attrs[:parent], | |||||
| original_created_from: attrs[:original_created_from], | |||||
| original_created_before: attrs[:original_created_before], | |||||
| created_at: Time.current, | |||||
| created_by_user: @created_by_user) | |||||
| end | |||||
| super(record: post, event_type:, created_by_user:) | |||||
| end | end | ||||
| private | private | ||||
| def version_class = PostVersion | |||||
| def version_association = :post_versions | |||||
| def record_key = :post | |||||
| def snapshot_attributes | def snapshot_attributes | ||||
| { title: @post.title, | { title: @post.title, | ||||
| url: @post.url, | url: @post.url, | ||||
| @@ -43,15 +22,4 @@ class PostVersionRecorder | |||||
| original_created_from: @post.original_created_from, | original_created_from: @post.original_created_from, | ||||
| original_created_before: @post.original_created_before } | original_created_before: @post.original_created_before } | ||||
| end | end | ||||
| def same_snapshot? version, attrs | |||||
| true && | |||||
| version.title == attrs[:title] && | |||||
| version.url == attrs[:url] && | |||||
| version.thumbnail_base == attrs[:thumbnail_base] && | |||||
| version.tags == attrs[:tags] && | |||||
| version.parent_id == attrs[:parent]&.id && | |||||
| version.original_created_from == attrs[:original_created_from] && | |||||
| version.original_created_before == attrs[:original_created_before] | |||||
| end | |||||
| end | end | ||||
| @@ -0,0 +1,22 @@ | |||||
| class TagVersionRecorder < VersionRecorder | |||||
| def self.record! tag:, event_type:, created_by_user: | |||||
| new(tag:, event_type:, created_by_user:).record! | |||||
| end | |||||
| def initialize tag:, event_type:, created_by_user: | |||||
| super(record: tag, event_type:, created_by_user:) | |||||
| end | |||||
| private | |||||
| def version_class = TagVersion | |||||
| def version_association = :tag_versions | |||||
| def record_key = :tag | |||||
| def snapshot_attributes | |||||
| { name: @tag.name, | |||||
| category: @tag.category, | |||||
| aliases: @tag.snapshot_aliases.join(' '), | |||||
| parent_tag_ids: @tag.snapshot_parent_tag_ids.join(' ') } | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,57 @@ | |||||
| class VersionRecorder | |||||
| EVENT_TYPES = ['create', 'update', 'discard', 'restore'].freeze | |||||
| def initialize record:, event_type:, created_by_user: | |||||
| @record = record | |||||
| @event_type = event_type.to_s | |||||
| @created_by_user = created_by_user | |||||
| validate_event_type! | |||||
| end | |||||
| def record! record, event_type:, created_by_user: | |||||
| @record.with_lock do | |||||
| latest = latest_version | |||||
| if !(latest) && @event_type != 'create' | |||||
| raise "#{ version_class.name } first event must be create" | |||||
| end | |||||
| if @event_type == 'create' && latest | |||||
| raise "#{ version_class.name } create event already exists" | |||||
| end | |||||
| attrs = snapshot_attributes | |||||
| return latest if @event_type == 'update' && latest && same_snapshot?(latest, attrs) | |||||
| version_class.create!(base_attributes(latest).merge(record_key => @record).merge(attrs)) | |||||
| end | |||||
| end | |||||
| private | |||||
| def latest_version = versions.order(version_no: :desc).first | |||||
| def versions = @record.public_send(version_association) | |||||
| def base_attributes latest | |||||
| { version_no: (latest&.version_no || 0) + 1, | |||||
| event_type: @event_type, | |||||
| created_at: Time.current, | |||||
| created_by_user: @created_by_user } | |||||
| end | |||||
| def same_snapshot?(version, attrs) = attrs.all? { |k, v| version.public_send(k) == v } | |||||
| def validate_event_type! | |||||
| return if EVENT_TYPES.include?(@event_type) | |||||
| raise ArgumentError, "Invalid event_type: #{ @event_type }" | |||||
| end | |||||
| def version_class = raise NotImplementedError | |||||
| def version_association = raise NotImplementedError | |||||
| def record_key = raise NotImplementedError | |||||
| def snapshot_attributes = raise NotImplementedError | |||||
| end | |||||
| @@ -15,6 +15,14 @@ class CreateTagVersions < ActiveRecord::Migration[8.0] | |||||
| self.table_name = 'tag_versions' | self.table_name = 'tag_versions' | ||||
| end | end | ||||
| class NicoTagVersion < ActiveRecord::Base | |||||
| self.table_name = 'nico_tag_versions' | |||||
| end | |||||
| class NicoTagRelation < ActiveRecord::Base | |||||
| self.table_name = 'nico_tag_relations' | |||||
| end | |||||
| def up | def up | ||||
| create_table :tag_versions do |t| | create_table :tag_versions do |t| | ||||
| t.references :tag, null: false, foreign_key: true, index: false | t.references :tag, null: false, foreign_key: true, index: false | ||||
| @@ -35,10 +43,28 @@ class CreateTagVersions < ActiveRecord::Migration[8.0] | |||||
| name: 'tag_versions_version_no_positive' | name: 'tag_versions_version_no_positive' | ||||
| end | end | ||||
| TagVersion.reset_column_information | |||||
| create_table :nico_tag_versions do |t| | |||||
| t.references :tag, null: false, foreign_key: true, index: false | |||||
| t.integer :version_no, null: false | |||||
| t.string :event_type, null: false | |||||
| t.string :name, null: false | |||||
| t.text :linked_tags, null: false | |||||
| t.datetime :created_at, null: false | |||||
| t.references :created_by_user, foreign_key: { to_table: :users }, index: false | |||||
| t.index [:tag_id, :version_no], unique: true | |||||
| t.index :created_at | |||||
| t.index [:tag_id, :created_at], order: { created_at: :desc } | |||||
| t.index [:created_by_user_id, :created_at], order: { created_at: :desc } | |||||
| t.check_constraint 'version_no > 0', | |||||
| name: 'nico_tag_versions_version_no_positive' | |||||
| end | |||||
| TagVersion.reset_column_information | |||||
| say_with_time 'Backfilling tag_versions' do | say_with_time 'Backfilling tag_versions' do | ||||
| Tag.where(discarded_at: nil).find_in_batches(batch_size: 500) do |tags| | |||||
| Tag.where(discarded_at: nil) | |||||
| .where.not(category: 'nico') | |||||
| .find_in_batches(batch_size: 500) do |tags| | |||||
| tag_ids = tags.map(&:id) | tag_ids = tags.map(&:id) | ||||
| tag_implication_rows_by_tag_id = | tag_implication_rows_by_tag_id = | ||||
| @@ -74,16 +100,57 @@ class CreateTagVersions < ActiveRecord::Migration[8.0] | |||||
| event_type: 'create', | event_type: 'create', | ||||
| name: tag_name_rows_by_tag_id[tag.id], | name: tag_name_rows_by_tag_id[tag.id], | ||||
| category: tag.category, | category: tag.category, | ||||
| aliases: tag_alias_rows_by_tag_id[tag.id].sort.join(' '), | |||||
| aliases: tag_alias_rows_by_tag_id[tag.id].sort.join(' '), | |||||
| parent_tag_ids: tag_implication_rows_by_tag_id[tag.id].sort.join(' '), | parent_tag_ids: tag_implication_rows_by_tag_id[tag.id].sort.join(' '), | ||||
| created_at: tag.created_at, | created_at: tag.created_at, | ||||
| created_by_user_id: nil } | created_by_user_id: nil } | ||||
| }) | }) | ||||
| end | end | ||||
| end | end | ||||
| NicoTagVersion.reset_column_information | |||||
| say_with_time 'Backfilling nico_tag_versions' do | |||||
| Tag.where(discarded_at: nil, category: 'nico') | |||||
| .find_in_batches(batch_size: 500) do |tags| | |||||
| tag_ids = tags.map(&:id) | |||||
| tag_name_rows_by_tag_id = | |||||
| TagName | |||||
| .joins('INNER JOIN tags ON tags.tag_name_id = tag_names.id') | |||||
| .where(tags: { id: tag_ids }) | |||||
| .pluck('tags.id', 'tag_names.name') | |||||
| .each_with_object({ }) do |row, h| | |||||
| h[row[0]] = row[1] | |||||
| end | |||||
| nico_tag_relation_rows_by_tag_id = | |||||
| NicoTagRelation | |||||
| .joins('INNER JOIN tags nico_tags ON nico_tags.id = nico_tag_relations.nico_tag_id') | |||||
| .joins('INNER JOIN tags linked_tags ON linked_tags.id = nico_tag_relations.tag_id') | |||||
| .joins('INNER JOIN tag_names ON tag_names.id = linked_tags.tag_name_id') | |||||
| .where(nico_tags: { id: tag_ids }) | |||||
| .where(linked_tags: { discarded_at: nil }) | |||||
| .where(tag_names: { discarded_at: nil }) | |||||
| .pluck('nico_tags.id', 'tag_names.name') | |||||
| .each_with_object(Hash.new { |h, k| h[k] = [] }) do |row, h| | |||||
| h[row[0]] << row[1] | |||||
| end | |||||
| NicoTagVersion.insert_all(tags.map { |tag| | |||||
| { tag_id: tag.id, | |||||
| version_no: 1, | |||||
| event_type: 'create', | |||||
| name: tag_name_rows_by_tag_id[tag.id], | |||||
| linked_tags: nico_tag_relation_rows_by_tag_id[tag.id].sort.join(' '), | |||||
| created_at: tag.created_at, | |||||
| created_by_user_id: nil } | |||||
| }) | |||||
| end | |||||
| end | |||||
| end | end | ||||
| def down | def down | ||||
| drop_table :nico_tag_versions | |||||
| drop_table :tag_versions | drop_table :tag_versions | ||||
| end | end | ||||
| end | end | ||||
| @@ -104,6 +104,21 @@ ActiveRecord::Schema[8.0].define(version: 2026_04_19_035400) do | |||||
| t.index ["tag_id"], name: "index_nico_tag_relations_on_tag_id" | t.index ["tag_id"], name: "index_nico_tag_relations_on_tag_id" | ||||
| end | end | ||||
| create_table "nico_tag_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | |||||
| t.bigint "tag_id", null: false | |||||
| t.integer "version_no", null: false | |||||
| t.string "event_type", null: false | |||||
| t.string "name", null: false | |||||
| t.text "linked_tags", null: false | |||||
| 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 ["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_similarities", primary_key: ["post_id", "target_post_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | 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 "post_id", null: false | ||||
| t.bigint "target_post_id", null: false | t.bigint "target_post_id", null: false | ||||
| @@ -394,6 +409,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_04_19_035400) do | |||||
| add_foreign_key "materials", "users", column: "updated_by_user_id" | add_foreign_key "materials", "users", column: "updated_by_user_id" | ||||
| add_foreign_key "nico_tag_relations", "tags" | add_foreign_key "nico_tag_relations", "tags" | ||||
| add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id" | add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id" | ||||
| add_foreign_key "nico_tag_versions", "tags" | |||||
| add_foreign_key "nico_tag_versions", "users", column: "created_by_user_id" | |||||
| add_foreign_key "post_similarities", "posts" | add_foreign_key "post_similarities", "posts" | ||||
| add_foreign_key "post_similarities", "posts", column: "target_post_id" | add_foreign_key "post_similarities", "posts", column: "target_post_id" | ||||
| add_foreign_key "post_tags", "posts" | add_foreign_key "post_tags", "posts" | ||||
| @@ -115,6 +115,10 @@ namespace :nico do | |||||
| datum['tags'].each do |raw| | datum['tags'].each do |raw| | ||||
| name = TagNameSanitisationRule.sanitise("nico:#{ 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!(name, category: :nico) | ||||
| event_type = tag.nico_tag_versions.exists? ? :update : :create | |||||
| NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user: nil) | |||||
| desired_nico_tag_based_ids << tag.id | desired_nico_tag_based_ids << tag.id | ||||
| # 新たに記載される外部タグと連携される内部タグを記載 | # 新たに記載される外部タグと連携される内部タグを記載 | ||||