投稿に対する履歴(#264) (#307)
Merge branch 'main' into feature/264 #264 #264 #264 #264 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #307
This commit was merged in pull request #307.
This commit is contained in:
@@ -127,17 +127,20 @@ class PostsController < ApplicationController
|
|||||||
post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user,
|
post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user,
|
||||||
original_created_from:, original_created_before:)
|
original_created_from:, original_created_before:)
|
||||||
post.thumbnail.attach(thumbnail)
|
post.thumbnail.attach(thumbnail)
|
||||||
if post.save
|
|
||||||
post.resized_thumbnail!
|
ActiveRecord::Base.transaction do
|
||||||
|
post.save!
|
||||||
tags = Tag.normalise_tags(tag_names)
|
tags = Tag.normalise_tags(tag_names)
|
||||||
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.reload
|
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: current_user)
|
||||||
render json: PostRepr.base(post), status: :created
|
|
||||||
else
|
|
||||||
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
|
|
||||||
end
|
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
|
rescue Tag::NicoTagNormalisationError
|
||||||
head :bad_request
|
head :bad_request
|
||||||
end
|
end
|
||||||
@@ -166,19 +169,22 @@ class PostsController < ApplicationController
|
|||||||
original_created_before = params[:original_created_before]
|
original_created_before = params[:original_created_before]
|
||||||
|
|
||||||
post = Post.find(params[:id].to_i)
|
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 +
|
tags = post.tags.where(category: 'nico').to_a +
|
||||||
Tag.normalise_tags(tag_names, with_tagme: false)
|
Tag.normalise_tags(tag_names, with_tagme: false)
|
||||||
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)
|
||||||
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
|
|
||||||
end
|
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
|
rescue Tag::NicoTagNormalisationError
|
||||||
head :bad_request
|
head :bad_request
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
has_many :user_post_views, dependent: :delete_all
|
has_many :user_post_views, dependent: :delete_all
|
||||||
has_many :post_similarities, dependent: :delete_all
|
has_many :post_similarities, dependent: :delete_all
|
||||||
|
has_many :post_versions
|
||||||
|
|
||||||
has_one_attached :thumbnail
|
has_one_attached :thumbnail
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ class Post < ApplicationRecord
|
|||||||
super(options).merge(thumbnail: nil)
|
super(options).merge(thumbnail: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def snapshot_tag_names = tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||||
|
|
||||||
def related limit: nil
|
def related limit: nil
|
||||||
ids = post_similarities.order(cos: :desc)
|
ids = post_similarities.order(cos: :desc)
|
||||||
ids = ids.limit(limit) if limit
|
ids = ids.limit(limit) if limit
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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' }, 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
|
||||||
|
|
||||||
|
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
|
class Tag < ApplicationRecord
|
||||||
include MyDiscard
|
include MyDiscard
|
||||||
|
|
||||||
@@ -150,6 +153,8 @@ class Tag < ApplicationRecord
|
|||||||
def self.merge_tags! target_tag, source_tags
|
def self.merge_tags! target_tag, source_tags
|
||||||
target_tag => Tag
|
target_tag => Tag
|
||||||
|
|
||||||
|
affected_post_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|
|
||||||
source_tag => Tag
|
source_tag => Tag
|
||||||
@@ -158,6 +163,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
|
||||||
source_pt.discard_by!(nil)
|
source_pt.discard_by!(nil)
|
||||||
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)
|
||||||
@@ -180,6 +186,10 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
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)
|
target_tag.update_columns(post_count: PostTag.kept.where(tag: target_tag).count)
|
||||||
end
|
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_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
|
||||||
|
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,203 @@
|
|||||||
|
require 'set'
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePostVersions < ActiveRecord::Migration[8.0]
|
||||||
|
class Post < ApplicationRecord
|
||||||
|
self.table_name = 'posts'
|
||||||
|
end
|
||||||
|
|
||||||
|
class PostTag < ApplicationRecord
|
||||||
|
self.table_name = 'post_tags'
|
||||||
|
end
|
||||||
|
|
||||||
|
class PostVersion < ApplicationRecord
|
||||||
|
self.table_name = 'post_versions'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
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',
|
||||||
|
name: 'post_versions_version_no_positive'
|
||||||
|
t.check_constraint "event_type IN ('create', 'update', 'discard', 'restore')",
|
||||||
|
name: 'post_versions_event_type_valid'
|
||||||
|
end
|
||||||
|
|
||||||
|
PostVersion.reset_column_information
|
||||||
|
|
||||||
|
say_with_time 'Backfilling post_versions' do
|
||||||
|
Post.find_in_batches(batch_size: 500) do |posts|
|
||||||
|
post_ids = posts.map(&:id)
|
||||||
|
|
||||||
|
post_tag_rows_by_post_id =
|
||||||
|
PostTag
|
||||||
|
.joins('INNER JOIN tags ON tags.id = post_tags.tag_id')
|
||||||
|
.joins('INNER JOIN tag_names ON tag_names.id = tags.tag_name_id')
|
||||||
|
.where(post_id: post_ids)
|
||||||
|
.pluck('post_tags.post_id',
|
||||||
|
'post_tags.created_at',
|
||||||
|
'post_tags.discarded_at',
|
||||||
|
'post_tags.created_user_id',
|
||||||
|
'post_tags.deleted_user_id',
|
||||||
|
'tag_names.name')
|
||||||
|
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |row, h|
|
||||||
|
post_id, created_at, discarded_at, created_user_id, deleted_user_id, tag_name = row
|
||||||
|
h[post_id] << { created_at:,
|
||||||
|
discarded_at:,
|
||||||
|
created_user_id:,
|
||||||
|
deleted_user_id:,
|
||||||
|
tag_name: }
|
||||||
|
end
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
|
||||||
|
posts.each do |post|
|
||||||
|
post_tag_rows = post_tag_rows_by_post_id[post.id]
|
||||||
|
|
||||||
|
events = post_tag_rows.flat_map do |post_tag_row|
|
||||||
|
ary = [[post_tag_row[:created_at],
|
||||||
|
post_tag_row[:created_user_id],
|
||||||
|
:add,
|
||||||
|
post_tag_row[:tag_name]]]
|
||||||
|
|
||||||
|
if post_tag_row[:discarded_at]
|
||||||
|
ary << [post_tag_row[:discarded_at],
|
||||||
|
post_tag_row[:deleted_user_id],
|
||||||
|
:remove,
|
||||||
|
post_tag_row[:tag_name]]
|
||||||
|
end
|
||||||
|
|
||||||
|
ary
|
||||||
|
end
|
||||||
|
|
||||||
|
kind_order = { add: 0, remove: 1 }
|
||||||
|
|
||||||
|
events.sort_by! do |event_at, user_id, kind, tag_name|
|
||||||
|
[event_at, user_id || 0, kind_order.fetch(kind), tag_name]
|
||||||
|
end
|
||||||
|
|
||||||
|
event_buckets = bucket_events(events)
|
||||||
|
|
||||||
|
active_tags = Set.new
|
||||||
|
version_no = 0
|
||||||
|
|
||||||
|
if event_buckets.empty?
|
||||||
|
version_no += 1
|
||||||
|
rows << build_row(post:,
|
||||||
|
version_no:,
|
||||||
|
event_type: 'create',
|
||||||
|
created_at: post.created_at,
|
||||||
|
created_by_user_id: post.uploaded_user_id,
|
||||||
|
tags: [])
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
first_bucket = event_buckets.first
|
||||||
|
merge_first_bucket_into_create = first_bucket[:first_at] <= post.created_at + 1.second
|
||||||
|
|
||||||
|
if merge_first_bucket_into_create
|
||||||
|
event_buckets.shift
|
||||||
|
apply_bucket!(active_tags, first_bucket)
|
||||||
|
|
||||||
|
version_no += 1
|
||||||
|
rows << build_row(
|
||||||
|
post:,
|
||||||
|
version_no:,
|
||||||
|
event_type: 'create',
|
||||||
|
created_at: post.created_at,
|
||||||
|
created_by_user_id: post.uploaded_user_id || first_bucket[:user_ids].compact.first,
|
||||||
|
tags: active_tags.to_a.sort)
|
||||||
|
else
|
||||||
|
version_no += 1
|
||||||
|
rows << build_row(
|
||||||
|
post:,
|
||||||
|
version_no:,
|
||||||
|
event_type: 'create',
|
||||||
|
created_at: post.created_at,
|
||||||
|
created_by_user_id: post.uploaded_user_id,
|
||||||
|
tags: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
event_buckets.each do |bucket|
|
||||||
|
apply_bucket!(active_tags, bucket)
|
||||||
|
|
||||||
|
version_no += 1
|
||||||
|
rows << build_row(
|
||||||
|
post:,
|
||||||
|
version_no:,
|
||||||
|
event_type: 'update',
|
||||||
|
created_at: bucket[:first_at],
|
||||||
|
created_by_user_id: bucket[:user_ids].compact.first,
|
||||||
|
tags: active_tags.to_a.sort)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PostVersion.insert_all!(rows) if rows.any?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :post_versions
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def bucket_events events
|
||||||
|
buckets = []
|
||||||
|
|
||||||
|
events.each do |event_at, user_id, kind, tag_name|
|
||||||
|
if buckets.empty? || event_at - buckets.last[:last_at] > 1.second
|
||||||
|
buckets << { first_at: event_at,
|
||||||
|
last_at: event_at,
|
||||||
|
user_ids: [user_id],
|
||||||
|
events: [[kind, tag_name]] }
|
||||||
|
else
|
||||||
|
bucket = buckets.last
|
||||||
|
bucket[:last_at] = event_at
|
||||||
|
bucket[:user_ids] << user_id
|
||||||
|
bucket[:events] << [kind, tag_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
buckets
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_bucket! active_tags, bucket
|
||||||
|
bucket[:events].each do |kind, tag_name|
|
||||||
|
if kind == :add
|
||||||
|
active_tags.add(tag_name)
|
||||||
|
else
|
||||||
|
active_tags.delete(tag_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_row post:, version_no:, event_type:, created_at:, created_by_user_id:, tags:
|
||||||
|
{ post_id: post.id,
|
||||||
|
version_no:,
|
||||||
|
event_type:,
|
||||||
|
title: post.title,
|
||||||
|
url: post.url,
|
||||||
|
thumbnail_base: post.thumbnail_base,
|
||||||
|
tags: tags.join(' '),
|
||||||
|
parent_id: post.parent_id,
|
||||||
|
original_created_from: post.original_created_from,
|
||||||
|
original_created_before: post.original_created_before,
|
||||||
|
created_at:,
|
||||||
|
created_by_user_id: }
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+25
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_03_29_034700) do
|
ActiveRecord::Schema[8.0].define(version: 2026_04_09_123700) do
|
||||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -132,6 +132,27 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_29_034700) do
|
|||||||
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "post_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.bigint "post_id", null: false
|
||||||
|
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.bigint "parent_id"
|
||||||
|
t.datetime "original_created_from"
|
||||||
|
t.datetime "original_created_before"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.bigint "created_by_user_id"
|
||||||
|
t.index ["created_by_user_id"], name: "index_post_versions_on_created_by_user_id"
|
||||||
|
t.index ["parent_id"], name: "index_post_versions_on_parent_id"
|
||||||
|
t.index ["post_id", "version_no"], name: "index_post_versions_on_post_id_and_version_no", unique: true
|
||||||
|
t.index ["post_id"], name: "index_post_versions_on_post_id"
|
||||||
|
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"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "title"
|
t.string "title"
|
||||||
t.string "url", limit: 768, null: false
|
t.string "url", limit: 768, null: false
|
||||||
@@ -362,6 +383,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_29_034700) do
|
|||||||
add_foreign_key "post_tags", "tags"
|
add_foreign_key "post_tags", "tags"
|
||||||
add_foreign_key "post_tags", "users", column: "created_user_id"
|
add_foreign_key "post_tags", "users", column: "created_user_id"
|
||||||
add_foreign_key "post_tags", "users", column: "deleted_user_id"
|
add_foreign_key "post_tags", "users", column: "deleted_user_id"
|
||||||
|
add_foreign_key "post_versions", "posts"
|
||||||
|
add_foreign_key "post_versions", "posts", column: "parent_id"
|
||||||
|
add_foreign_key "post_versions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "posts", "posts", column: "parent_id"
|
add_foreign_key "posts", "posts", column: "parent_id"
|
||||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||||
add_foreign_key "settings", "users"
|
add_foreign_key "settings", "users"
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ namespace :nico do
|
|||||||
original_created_from = original_created_at&.change(sec: 0)
|
original_created_from = original_created_at&.change(sec: 0)
|
||||||
original_created_before = original_created_from&.+(1.minute)
|
original_created_before = original_created_from&.+(1.minute)
|
||||||
|
|
||||||
|
post_created = false
|
||||||
|
post_changed = false
|
||||||
|
|
||||||
if post
|
if post
|
||||||
attrs = { title:, original_created_from:, original_created_before: }
|
attrs = { title:, original_created_from:, original_created_before: }
|
||||||
|
|
||||||
@@ -76,11 +79,13 @@ namespace :nico do
|
|||||||
end
|
end
|
||||||
|
|
||||||
post.assign_attributes(attrs)
|
post.assign_attributes(attrs)
|
||||||
if post.changed?
|
post_changed = post.changed?
|
||||||
|
if post_changed
|
||||||
post.save!
|
post.save!
|
||||||
post.resized_thumbnail! if post.thumbnail.attached?
|
post.resized_thumbnail! if post.thumbnail.attached?
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
post_created = true
|
||||||
url = "https://www.nicovideo.jp/watch/#{ code }"
|
url = "https://www.nicovideo.jp/watch/#{ code }"
|
||||||
thumbnail_base = fetch_thumbnail.(url) rescue nil
|
thumbnail_base = fetch_thumbnail.(url) rescue nil
|
||||||
post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil,
|
post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil,
|
||||||
@@ -140,6 +145,12 @@ namespace :nico do
|
|||||||
desired_all_tag_ids.uniq!
|
desired_all_tag_ids.uniq!
|
||||||
|
|
||||||
sync_post_tags!(post, desired_all_tag_ids, current_tag_ids: kept_tag_ids)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe PostVersion, type: :model do
|
||||||
|
let!(:tag_name) { TagName.create!(name: 'post_version_spec_tag') }
|
||||||
|
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
|
||||||
|
|
||||||
|
let!(:post_record) do
|
||||||
|
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
|
||||||
|
PostTag.create!(post: post, tag: tag)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:post_version) do
|
||||||
|
PostVersion.create!(
|
||||||
|
post: post_record,
|
||||||
|
version_no: 1,
|
||||||
|
event_type: 'create',
|
||||||
|
title: post_record.title,
|
||||||
|
url: post_record.url,
|
||||||
|
thumbnail_base: post_record.thumbnail_base,
|
||||||
|
tags: post_record.snapshot_tag_names.join(' '),
|
||||||
|
parent: post_record.parent,
|
||||||
|
original_created_from: post_record.original_created_from,
|
||||||
|
original_created_before: post_record.original_created_before,
|
||||||
|
created_at: Time.current,
|
||||||
|
created_by_user: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is read only after create' do
|
||||||
|
expect do
|
||||||
|
post_version.update!(title: 'changed')
|
||||||
|
end.to raise_error(ActiveRecord::ReadOnlyRecord)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'cannot be destroyed' do
|
||||||
|
expect do
|
||||||
|
post_version.destroy!
|
||||||
|
end.to raise_error(ActiveRecord::ReadOnlyRecord)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -795,4 +795,127 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(user.reload.viewed?(post_record)).to be(false)
|
expect(user.reload.viewed?(post_record)).to be(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'post versioning' do
|
||||||
|
let(:member) { create(:user, :member) }
|
||||||
|
|
||||||
|
def snapshot_tags(post)
|
||||||
|
post.snapshot_tag_names.join(' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_post_version_for!(post)
|
||||||
|
PostVersion.create!(
|
||||||
|
post: post,
|
||||||
|
version_no: 1,
|
||||||
|
event_type: 'create',
|
||||||
|
title: post.title,
|
||||||
|
url: post.url,
|
||||||
|
thumbnail_base: post.thumbnail_base,
|
||||||
|
tags: snapshot_tags(post),
|
||||||
|
parent: post.parent,
|
||||||
|
original_created_from: post.original_created_from,
|
||||||
|
original_created_before: post.original_created_before,
|
||||||
|
created_at: post.created_at,
|
||||||
|
created_by_user: post.uploaded_user
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates version 1 on POST /posts' do
|
||||||
|
sign_in_as(member)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post '/posts', params: {
|
||||||
|
title: 'versioned post',
|
||||||
|
url: 'https://example.com/versioned-post',
|
||||||
|
tags: 'spec_tag',
|
||||||
|
thumbnail: dummy_upload
|
||||||
|
}
|
||||||
|
end.to change(PostVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
|
created_post = Post.find(json.fetch('id'))
|
||||||
|
version = PostVersion.find_by!(post: created_post, version_no: 1)
|
||||||
|
|
||||||
|
expect(version.event_type).to eq('create')
|
||||||
|
expect(version.title).to eq('versioned post')
|
||||||
|
expect(version.url).to eq('https://example.com/versioned-post')
|
||||||
|
expect(version.created_by_user_id).to eq(member.id)
|
||||||
|
expect(version.tags).to eq(snapshot_tags(created_post))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates next version on PUT /posts/:id when snapshot changes' do
|
||||||
|
sign_in_as(member)
|
||||||
|
create_post_version_for!(post_record)
|
||||||
|
|
||||||
|
tag_name2 = TagName.create!(name: 'spec_tag_2')
|
||||||
|
Tag.create!(tag_name: tag_name2, category: :general)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
put "/posts/#{post_record.id}", params: {
|
||||||
|
title: 'updated title',
|
||||||
|
tags: 'spec_tag_2'
|
||||||
|
}
|
||||||
|
end.to change(PostVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
version = post_record.reload.post_versions.order(:version_no).last
|
||||||
|
expect(version.version_no).to eq(2)
|
||||||
|
expect(version.event_type).to eq('update')
|
||||||
|
expect(version.title).to eq('updated title')
|
||||||
|
expect(version.created_by_user_id).to eq(member.id)
|
||||||
|
expect(version.tags).to eq(snapshot_tags(post_record.reload))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a new version on PUT /posts/:id when snapshot is unchanged' do
|
||||||
|
sign_in_as(member)
|
||||||
|
create_post_version_for!(post_record)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
put "/posts/#{post_record.id}", params: {
|
||||||
|
title: post_record.title,
|
||||||
|
tags: 'spec_tag'
|
||||||
|
}
|
||||||
|
end.not_to change(PostVersion, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
version = post_record.reload.post_versions.order(:version_no).last
|
||||||
|
expect(version.version_no).to eq(1)
|
||||||
|
expect(version.event_type).to eq('create')
|
||||||
|
expect(version.tags).to eq(snapshot_tags(post_record))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a version when POST /posts is invalid' do
|
||||||
|
sign_in_as(member)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post '/posts', params: {
|
||||||
|
title: 'invalid post',
|
||||||
|
url: 'ぼざクリタグ広場',
|
||||||
|
tags: 'spec_tag',
|
||||||
|
thumbnail: dummy_upload
|
||||||
|
}
|
||||||
|
end.not_to change(PostVersion, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a version when PUT /posts/:id is invalid' do
|
||||||
|
sign_in_as(member)
|
||||||
|
create_post_version_for!(post_record)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
put "/posts/#{post_record.id}", params: {
|
||||||
|
title: 'updated title',
|
||||||
|
tags: 'spec_tag',
|
||||||
|
original_created_from: Time.zone.local(2020, 1, 2, 0, 0, 0).iso8601,
|
||||||
|
original_created_before: Time.zone.local(2020, 1, 1, 0, 0, 0).iso8601
|
||||||
|
}
|
||||||
|
end.not_to change(PostVersion, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user