| @@ -127,17 +127,20 @@ class PostsController < ApplicationController | |||
| post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user, | |||
| original_created_from:, original_created_before:) | |||
| post.thumbnail.attach(thumbnail) | |||
| if post.save | |||
| post.resized_thumbnail! | |||
| ActiveRecord::Base.transaction do | |||
| post.save! | |||
| tags = Tag.normalise_tags(tag_names) | |||
| tags = Tag.expand_parent_tags(tags) | |||
| sync_post_tags!(post, tags) | |||
| post.reload | |||
| render json: PostRepr.base(post), status: :created | |||
| else | |||
| render json: { errors: post.errors.full_messages }, status: :unprocessable_entity | |||
| post.resized_thumbnail! | |||
| PostVersionRecorder.record!(post:, event_type: :create, created_by_user: current_user) | |||
| end | |||
| post.reload | |||
| render json: PostRepr.base(post), status: :created | |||
| rescue ActiveRecord::RecordInvalid | |||
| render json: { errors: post.errors.full_messages }, status: :unprocessable_entity | |||
| rescue Tag::NicoTagNormalisationError | |||
| head :bad_request | |||
| end | |||
| @@ -166,19 +169,22 @@ class PostsController < ApplicationController | |||
| original_created_before = params[:original_created_before] | |||
| post = Post.find(params[:id].to_i) | |||
| if post.update(title:, original_created_from:, original_created_before:) | |||
| ActiveRecord::Base.transaction do | |||
| 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) | |||
| tags = Tag.expand_parent_tags(tags) | |||
| sync_post_tags!(post, tags) | |||
| post.reload | |||
| json = post.as_json | |||
| json['tags'] = build_tag_tree_for(post.tags) | |||
| render json:, status: :ok | |||
| else | |||
| render json: post.errors, status: :unprocessable_entity | |||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user) | |||
| end | |||
| post.reload | |||
| json = post.as_json | |||
| json['tags'] = build_tag_tree_for(post.tags) | |||
| render json:, status: :ok | |||
| rescue ActiveRecord::RecordInvalid | |||
| render json: post.errors, status: :unprocessable_entity | |||
| rescue Tag::NicoTagNormalisationError | |||
| head :bad_request | |||
| end | |||
| @@ -11,6 +11,7 @@ class Post < ApplicationRecord | |||
| has_many :user_post_views, dependent: :delete_all | |||
| has_many :post_similarities, dependent: :delete_all | |||
| has_many :post_versions | |||
| has_one_attached :thumbnail | |||
| @@ -30,6 +31,8 @@ class Post < ApplicationRecord | |||
| super(options).merge(thumbnail: nil) | |||
| end | |||
| def snapshot_tag_names = tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name') | |||
| def related limit: nil | |||
| ids = post_similarities.order(cos: :desc) | |||
| ids = ids.limit(limit) if limit | |||
| @@ -0,0 +1,35 @@ | |||
| class PostVersion < ApplicationRecord | |||
| before_update do | |||
| raise ActiveRecord::ReadOnlyRecord, '版は更新できません.' | |||
| end | |||
| before_destroy do | |||
| raise ActiveRecord::ReadOnlyRecord, '版は削除できません.' | |||
| end | |||
| belongs_to :post | |||
| 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' | |||
| validates :version_no, presence: true, numerically: { only_integer: true, greater_than: 0 } | |||
| validates :event_type, presence: true, inclusion: { in: event_types.keys } | |||
| validates :url, presence: true | |||
| validate :validate_original_created_range | |||
| scope :chronological, -> { order(:version_no, :id) } | |||
| private | |||
| def validate_original_created_range | |||
| f = original_created_from | |||
| b = original_created_before | |||
| return if f.blank? || b.blank? | |||
| if f >= b | |||
| errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.' | |||
| end | |||
| end | |||
| end | |||
| @@ -1,3 +1,6 @@ | |||
| require 'set' | |||
| class Tag < ApplicationRecord | |||
| include MyDiscard | |||
| @@ -150,6 +153,8 @@ class Tag < ApplicationRecord | |||
| def self.merge_tags! target_tag, source_tags | |||
| target_tag => Tag | |||
| affected_post_ids = Set.new | |||
| Tag.transaction do | |||
| Array(source_tags).compact.uniq.each do |source_tag| | |||
| source_tag => Tag | |||
| @@ -158,6 +163,7 @@ class Tag < ApplicationRecord | |||
| source_tag.post_tags.kept.find_each do |source_pt| | |||
| post_id = source_pt.post_id | |||
| affected_post_ids << post_id | |||
| source_pt.discard_by!(nil) | |||
| unless PostTag.kept.exists?(post_id:, tag: target_tag) | |||
| PostTag.create!(post_id:, tag: target_tag) | |||
| @@ -180,6 +186,10 @@ class Tag < ApplicationRecord | |||
| end | |||
| end | |||
| Post.where(id: affected_post_ids.to_a).find_each do |post| | |||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil) | |||
| end | |||
| # 投稿件数を再集計 | |||
| target_tag.update_columns(post_count: PostTag.kept.where(tag: target_tag).count) | |||
| end | |||
| @@ -0,0 +1,57 @@ | |||
| class PostVersionRecorder | |||
| def self.record! post:, event_type:, created_by_user: | |||
| new(post:, event_type:, created_by_user:).record! | |||
| end | |||
| def initialize post:, event_type:, created_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[:url], | |||
| 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 | |||
| end | |||
| private | |||
| def snapshot_attributes | |||
| { title: @post.title, | |||
| url: @post.url, | |||
| thumbnail_base: @post.thumbnail_base, | |||
| tags: @post.snapshot_tag_names.join(' '), | |||
| parent: @post.parent, | |||
| original_created_from: @post.original_created_from, | |||
| original_created_before: @post.original_created_before } | |||
| 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 | |||
| @@ -0,0 +1,80 @@ | |||
| class CreatePostVersions < ActiveRecord::Migration[8.0] | |||
| def change | |||
| create_table :post_versions do |t| | |||
| t.references :post, null: false, foreign_key: true | |||
| t.integer :version_no, null: false | |||
| t.string :event_type, null: false | |||
| t.string :title | |||
| t.string :url, limit: 768, null: false | |||
| t.string :thumbnail_base, limit: 2000 | |||
| t.text :tags, null: false | |||
| t.references :parent, foreign_key: { to_table: :posts } | |||
| t.datetime :original_created_from | |||
| t.datetime :original_created_before | |||
| t.datetime :created_at, null: false | |||
| t.references :created_by_user, foreign_key: { to_table: :users } | |||
| t.index [:post_id, :version_no], unique: true | |||
| t.check_constraint 'version_no > 0' | |||
| t.check_constraint "event_type IN ('create', 'update', 'discard', 'restore')" | |||
| end | |||
| reversible do |dir| | |||
| dir.up do | |||
| execute <<~SQL | |||
| INSERT INTO | |||
| post_versions( | |||
| post_id | |||
| , version_no | |||
| , event_type | |||
| , title | |||
| , url | |||
| , thumbnail_base | |||
| , tags | |||
| , parent_id | |||
| , original_created_from | |||
| , original_created_before | |||
| , created_at | |||
| , created_by_user_id) | |||
| SELECT | |||
| posts.id | |||
| , 1 | |||
| , 'create' | |||
| , posts.title | |||
| , posts.url | |||
| , posts.thumbnail_base | |||
| , COALESCE(tag_snapshots.tags, '') | |||
| , posts.parent_id | |||
| , posts.original_created_from | |||
| , posts.original_created_before | |||
| , posts.created_at | |||
| , posts.uploaded_user_id | |||
| FROM | |||
| posts | |||
| LEFT JOIN | |||
| ( | |||
| SELECT | |||
| post_tags.post_id | |||
| , GROUP_CONCAT(tag_names.name ORDER BY tag_names.name SEPARATOR ' ') AS tags | |||
| FROM | |||
| post_tags | |||
| INNER JOIN | |||
| tags | |||
| ON | |||
| tags.id = post_tags.tag_id | |||
| INNER JOIN | |||
| tag_names | |||
| ON | |||
| tag_names.id = tags.tag_name_id | |||
| WHERE | |||
| post_tags.discarded_at IS NULL | |||
| GROUP BY | |||
| post_tags.post_id | |||
| ) tag_snapshots | |||
| ON | |||
| tag_snapshots.post_id = posts.id | |||
| SQL | |||
| end | |||
| end | |||
| end | |||
| end | |||
| @@ -61,6 +61,9 @@ namespace :nico do | |||
| original_created_from = original_created_at&.change(sec: 0) | |||
| original_created_before = original_created_from&.+(1.minute) | |||
| post_created = false | |||
| post_changed = false | |||
| if post | |||
| attrs = { title:, original_created_from:, original_created_before: } | |||
| @@ -76,11 +79,13 @@ namespace :nico do | |||
| end | |||
| post.assign_attributes(attrs) | |||
| if post.changed? | |||
| post_changed = post.changed? | |||
| if post_changed | |||
| post.save! | |||
| post.resized_thumbnail! if post.thumbnail.attached? | |||
| end | |||
| else | |||
| post_created = true | |||
| url = "https://www.nicovideo.jp/watch/#{ code }" | |||
| thumbnail_base = fetch_thumbnail.(url) rescue nil | |||
| post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil, | |||
| @@ -140,6 +145,12 @@ namespace :nico do | |||
| desired_all_tag_ids.uniq! | |||
| sync_post_tags!(post, desired_all_tag_ids, current_tag_ids: kept_tag_ids) | |||
| if post_created | |||
| PostVersionRecorder.record!(post:, event_type: :create, created_by_user: nil) | |||
| elsif post_changed || kept_tag_ids != desired_all_tag_ids.to_set | |||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: nil) | |||
| end | |||
| end | |||
| end | |||
| end | |||