#171 #171 #171 #171 #171 #171 #171 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #345
This commit was merged in pull request #345.
This commit is contained in:
@@ -44,7 +44,7 @@ class PostsController < ApplicationController
|
||||
filtered_posts
|
||||
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
|
||||
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
|
||||
.preload(tags: [:materials, { tag_name: :wiki_page }])
|
||||
.preload(tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
.with_attached_thumbnail
|
||||
|
||||
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
||||
@@ -95,7 +95,7 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def random
|
||||
post = filtered_posts.preload(tags: [:materials, { tag_name: :wiki_page }])
|
||||
post = filtered_posts.preload(tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
.order('RAND()')
|
||||
.first
|
||||
return head :not_found unless post
|
||||
@@ -104,7 +104,7 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
post = Post.includes(tags: [:materials, { tag_name: :wiki_page }]).find_by(id: params[:id])
|
||||
post = Post.includes(tags: [:deerjikists, :materials, { tag_name: :wiki_page }]).find_by(id: params[:id])
|
||||
return head :not_found unless post
|
||||
|
||||
render json: PostRepr.base(post, current_user)
|
||||
@@ -173,33 +173,68 @@ class PostsController < ApplicationController
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
|
||||
force = bool?(:force)
|
||||
merge = bool?(:merge)
|
||||
return head :bad_request if force && merge
|
||||
|
||||
base_version_no = parse_base_version_no
|
||||
return head :bad_request if !(force) && !(base_version_no)
|
||||
|
||||
title = params[:title].presence
|
||||
tag_names = params[:tags].to_s.split
|
||||
original_created_from = params[:original_created_from]
|
||||
original_created_before = params[:original_created_before]
|
||||
parent_post_ids = parse_parent_post_ids
|
||||
|
||||
post = Post.find(params[:id].to_i)
|
||||
post = nil
|
||||
conflict_json = nil
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
||||
post = Post.lock.find(params[:id].to_i)
|
||||
|
||||
post.update!(title:, original_created_from:, original_created_before:)
|
||||
base_version = nil
|
||||
base_snapshot = nil
|
||||
current_snapshot = nil
|
||||
unless force
|
||||
base_version = post.post_versions.find_by!(version_no: base_version_no)
|
||||
|
||||
normalised_tags = Tag.normalise_tags!(tag_names, with_tagme: false)
|
||||
TagVersioning.record_tag_snapshots!(normalised_tags, created_by_user: current_user)
|
||||
base_snapshot = post_snapshot_from_version(base_version)
|
||||
current_snapshot = post_snapshot_from_record(post)
|
||||
end
|
||||
incoming_snapshot = post_incoming_snapshot(title:,
|
||||
original_created_from:,
|
||||
original_created_before:,
|
||||
tag_names:,
|
||||
parent_post_ids:)
|
||||
|
||||
tags = post.tags.nico.to_a + normalised_tags
|
||||
tags = Tag.expand_parent_tags(tags)
|
||||
sync_post_tags!(post, tags)
|
||||
snapshot_to_apply =
|
||||
if force || post.version_no == base_version_no || current_snapshot == base_snapshot
|
||||
incoming_snapshot
|
||||
else
|
||||
changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot)
|
||||
conflicts = changes.select { |change| change[:conflict] }
|
||||
|
||||
sync_parent_posts!(post, parent_post_ids)
|
||||
if merge && conflicts.empty?
|
||||
merge_post_snapshots(base_snapshot, current_snapshot, incoming_snapshot)
|
||||
else
|
||||
conflict_json = post_conflict_json(post:,
|
||||
base_version_no:,
|
||||
base_snapshot:,
|
||||
current_snapshot:,
|
||||
incoming_snapshot:,
|
||||
changes:,
|
||||
conflicts:)
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
|
||||
apply_post_snapshot!(post, snapshot_to_apply)
|
||||
end
|
||||
|
||||
return render json: conflict_json, status: :conflict if conflict_json
|
||||
|
||||
post.reload
|
||||
json = post.as_json
|
||||
json = PostRepr.base(post, current_user)
|
||||
json['tags'] = build_tag_tree_for(post.tags)
|
||||
render json:, status: :ok
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
@@ -225,7 +260,7 @@ class PostsController < ApplicationController
|
||||
pts = pts.where(post_id: id) if id.present?
|
||||
pts = pts.where(tag_id:) if tag_id.present?
|
||||
pts = pts.includes(:post, :created_user, :deleted_user,
|
||||
tag: [:materials, { tag_name: :wiki_page }])
|
||||
tag: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||
|
||||
events = []
|
||||
pts.each do |pt|
|
||||
@@ -404,4 +439,205 @@ class PostsController < ApplicationController
|
||||
PostImplication.create_or_find_by!(post_id: post.id, parent_post_id:)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_base_version_no
|
||||
version_no = Integer(params[:base_version_no], exception: false)
|
||||
if version_no&.positive?
|
||||
version_no
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def post_snapshot_from_version version
|
||||
{ title: version.title,
|
||||
original_created_from: snapshot_time(version.original_created_from),
|
||||
original_created_before: snapshot_time(version.original_created_before),
|
||||
tag_names: editable_tag_names_from_version(version),
|
||||
parent_post_ids: snapshot_parent_post_ids_from_version(version) }
|
||||
end
|
||||
|
||||
def editable_tag_names_from_version version
|
||||
version.tags.to_s.split.reject { |name| name.downcase.start_with?('nico:') }.sort
|
||||
end
|
||||
|
||||
def post_snapshot_from_record post
|
||||
{ title: post.title,
|
||||
original_created_from: snapshot_time(post.original_created_from),
|
||||
original_created_before: snapshot_time(post.original_created_before),
|
||||
tag_names: editable_tag_names_from_post(post),
|
||||
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
|
||||
end
|
||||
|
||||
def editable_tag_names_from_post post
|
||||
post.tags.not_nico.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
end
|
||||
|
||||
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
||||
tag_names:, parent_post_ids:
|
||||
{ title:,
|
||||
original_created_from: snapshot_time(original_created_from),
|
||||
original_created_before: snapshot_time(original_created_before),
|
||||
tag_names: incoming_tag_names_for_snapshot(tag_names),
|
||||
parent_post_ids: parent_post_ids.sort }
|
||||
end
|
||||
|
||||
def snapshot_parent_post_ids_from_version version
|
||||
if version.respond_to?(:parent_post_ids)
|
||||
version.parent_post_ids.to_s.split.map { |id| id.to_i }.sort
|
||||
elsif version.respond_to?(:parent_id) && version.parent_id
|
||||
[version.parent_id]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def snapshot_time value
|
||||
return nil if value.blank?
|
||||
|
||||
value = Time.zone.parse(value.to_s) if value in String
|
||||
value&.in_time_zone&.iso8601(6)
|
||||
rescue ArgumentError, TypeError
|
||||
value.to_s
|
||||
end
|
||||
|
||||
def incoming_tag_names_for_snapshot raw_tag_names
|
||||
tags = Tag.normalise_tags!(raw_tag_names, with_tagme: false)
|
||||
|
||||
Tag.expand_parent_tags(tags).map(&:name).uniq.sort
|
||||
end
|
||||
|
||||
def post_conflict_json post:, base_version_no:, base_snapshot:,
|
||||
current_snapshot:, incoming_snapshot:, changes:, conflicts:
|
||||
{ error: 'conflict',
|
||||
message: '競合が発生しました.',
|
||||
post_id: post.id,
|
||||
base_version_no:,
|
||||
current_version_no: post.version_no,
|
||||
base: base_snapshot,
|
||||
current: current_snapshot,
|
||||
mine: incoming_snapshot,
|
||||
changes:,
|
||||
conflicts:,
|
||||
mergeable: conflicts.empty? }
|
||||
end
|
||||
|
||||
def post_snapshot_changes base_snapshot, current_snapshot, incoming_snapshot
|
||||
[scalar_snapshot_change(:title, 'タイトル',
|
||||
base_snapshot, current_snapshot, incoming_snapshot),
|
||||
scalar_snapshot_change(:original_created_from, 'オリジナルの作成日時(以降)',
|
||||
base_snapshot, current_snapshot, incoming_snapshot),
|
||||
scalar_snapshot_change(:original_created_before, 'オリジナルの作成日時(より前)',
|
||||
base_snapshot, current_snapshot, incoming_snapshot),
|
||||
set_snapshot_change(:tag_names, 'タグ',
|
||||
base_snapshot, current_snapshot, incoming_snapshot),
|
||||
set_snapshot_change(:parent_post_ids, '親投稿',
|
||||
base_snapshot, current_snapshot, incoming_snapshot)].compact
|
||||
end
|
||||
|
||||
def scalar_snapshot_change field, label, base_snapshot, current_snapshot, incoming_snapshot
|
||||
base = base_snapshot[field]
|
||||
current = current_snapshot[field]
|
||||
mine = incoming_snapshot[field]
|
||||
|
||||
return nil if current == base && mine == base
|
||||
|
||||
{ field:, label:, base:, current:, mine:,
|
||||
changed_by_current: current != base,
|
||||
changed_by_me: mine != base,
|
||||
conflict: scalar_snapshot_conflict?(base, current, mine) }
|
||||
end
|
||||
|
||||
def scalar_snapshot_conflict? base, current, mine
|
||||
current != base && mine != base && current != mine
|
||||
end
|
||||
|
||||
def set_snapshot_change field, label, base_snapshot, current_snapshot, incoming_snapshot
|
||||
base = base_snapshot[field].to_a
|
||||
current = current_snapshot[field].to_a
|
||||
mine = incoming_snapshot[field].to_a
|
||||
|
||||
added_by_current = current - base
|
||||
removed_by_current = base - current
|
||||
added_by_me = mine - base
|
||||
removed_by_me = base - mine
|
||||
|
||||
if (added_by_current.empty? &&
|
||||
removed_by_current.empty? &&
|
||||
added_by_me.empty? &&
|
||||
removed_by_me.empty?)
|
||||
return nil
|
||||
end
|
||||
|
||||
{ field:, label:, base:, current:, mine:, added_by_current:, removed_by_current:,
|
||||
added_by_me:, removed_by_me:,
|
||||
changed_by_current: added_by_current.present? || removed_by_current.present?,
|
||||
changed_by_me: added_by_me.present? || removed_by_me.present?,
|
||||
conflict: set_snapshot_conflict?(added_by_current:,
|
||||
removed_by_current:,
|
||||
added_by_me:,
|
||||
removed_by_me:) }
|
||||
end
|
||||
|
||||
def set_snapshot_conflict? added_by_current:, removed_by_current:,
|
||||
added_by_me:, removed_by_me:
|
||||
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
|
||||
end
|
||||
|
||||
def apply_post_snapshot! post, snapshot
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
||||
|
||||
post.update!(title: snapshot[:title],
|
||||
original_created_from: snapshot[:original_created_from],
|
||||
original_created_before: snapshot[:original_created_before])
|
||||
|
||||
editable_tags = Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false)
|
||||
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
|
||||
|
||||
readonly_tags = post.tags.nico.to_a
|
||||
|
||||
tags = readonly_tags + editable_tags
|
||||
tags = Tag.expand_parent_tags(tags)
|
||||
|
||||
sync_post_tags!(post, tags)
|
||||
sync_parent_posts!(post, snapshot[:parent_post_ids])
|
||||
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
|
||||
end
|
||||
|
||||
def merge_post_snapshots base_snapshot, current_snapshot, incoming_snapshot
|
||||
[:title, :original_created_from, :original_created_before].map {
|
||||
[_1, merge_scalar_snapshot_value(base_snapshot[_1],
|
||||
current_snapshot[_1],
|
||||
incoming_snapshot[_1])]
|
||||
}.to_h.merge([:tag_names, :parent_post_ids].map {
|
||||
[_1, merge_set_snapshot_value(base_snapshot[_1],
|
||||
current_snapshot[_1],
|
||||
incoming_snapshot[_1])]
|
||||
}.to_h)
|
||||
end
|
||||
|
||||
def merge_scalar_snapshot_value base, current, mine
|
||||
return mine if current == base
|
||||
return current if mine == base || current == mine
|
||||
|
||||
raise ArgumentError, '競合してゐる項目はマージできません.'
|
||||
end
|
||||
|
||||
def merge_set_snapshot_value base, current, mine
|
||||
base = base.to_a
|
||||
current = current.to_a
|
||||
mine = mine.to_a
|
||||
|
||||
added_by_current = current - base
|
||||
removed_by_current = base - current
|
||||
added_by_me = mine - base
|
||||
removed_by_me = base - mine
|
||||
|
||||
merged = base + added_by_current + added_by_me
|
||||
merged -= removed_by_current
|
||||
merged -= removed_by_me
|
||||
|
||||
merged.uniq.sort
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,6 +28,8 @@ class Post < ApplicationRecord
|
||||
|
||||
has_one_attached :thumbnail
|
||||
|
||||
attribute :version_no, :integer, default: 1
|
||||
|
||||
before_validation :normalise_url
|
||||
|
||||
validates :url, presence: true, uniqueness: true
|
||||
|
||||
@@ -40,6 +40,8 @@ class Tag < ApplicationRecord
|
||||
belongs_to :tag_name
|
||||
delegate :wiki_page, to: :tag_name
|
||||
|
||||
attribute :version_no, :integer, default: 1
|
||||
|
||||
delegate :name, to: :tag_name, allow_nil: true
|
||||
validates :tag_name, presence: true
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ class WikiPage < ApplicationRecord
|
||||
|
||||
has_many :wiki_versions
|
||||
|
||||
attribute :version_no, :integer, default: 1
|
||||
|
||||
belongs_to :tag_name
|
||||
validates :tag_name, presence: true
|
||||
validates :body, presence: true
|
||||
|
||||
@@ -16,19 +16,20 @@ class VersionRecorder
|
||||
@record = record_class.unscoped.lock.find(@record.id)
|
||||
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
|
||||
validate_version_sequence!(latest)
|
||||
|
||||
attrs = snapshot_attributes
|
||||
|
||||
return latest if @event_type == 'update' && latest && same_snapshot?(latest, attrs)
|
||||
if @event_type == 'update' && latest && same_snapshot?(latest, attrs)
|
||||
return latest
|
||||
end
|
||||
|
||||
version_class.create!(base_attributes(latest).merge(record_key => @record).merge(attrs))
|
||||
version = version_class.create!(
|
||||
base_attributes(latest).merge(record_key => @record).merge(attrs))
|
||||
|
||||
update_record_version_no!(version.version_no)
|
||||
|
||||
version
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +46,31 @@ class VersionRecorder
|
||||
created_by_user: @created_by_user }
|
||||
end
|
||||
|
||||
def same_snapshot?(version, attrs) = attrs.all? { |k, v| version.public_send(k) == v }
|
||||
def update_record_version_no! version_no
|
||||
@record.update_columns(version_no:)
|
||||
@record.version_no = version_no
|
||||
end
|
||||
|
||||
def validate_version_sequence! latest
|
||||
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
|
||||
|
||||
return unless latest
|
||||
|
||||
if @record.version_no != latest.version_no
|
||||
raise ("#{ record_class.name }##{ @record.id } version_no is #{ @record.version_no }, " +
|
||||
"but latest #{ version_class.name } version_no is #{ latest.version_no }")
|
||||
end
|
||||
end
|
||||
|
||||
def same_snapshot? version, attrs
|
||||
attrs.all? { |k, v| version.public_send(k) == v }
|
||||
end
|
||||
|
||||
def validate_event_type!
|
||||
return if EVENT_TYPES.include?(@event_type)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class AddVersionNoToPosts < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
add_column :posts, :version_no, :integer
|
||||
|
||||
execute <<~SQL
|
||||
UPDATE
|
||||
posts
|
||||
SET
|
||||
version_no = (
|
||||
SELECT
|
||||
MAX(version_no)
|
||||
FROM
|
||||
post_versions
|
||||
WHERE
|
||||
post_id = posts.id)
|
||||
SQL
|
||||
|
||||
change_column_null :posts, :version_no, false
|
||||
|
||||
add_check_constraint :posts, 'version_no > 0', name: 'chk_posts_version_no_positive'
|
||||
end
|
||||
|
||||
def down
|
||||
remove_check_constraint :posts, name: 'chk_posts_version_no_positive'
|
||||
remove_column :posts, :version_no
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
class AddVersionNoToTags < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
add_column :tags, :version_no, :integer
|
||||
|
||||
execute <<~SQL
|
||||
UPDATE
|
||||
tags
|
||||
SET
|
||||
version_no = (
|
||||
CASE category
|
||||
WHEN 'nico' THEN
|
||||
(SELECT
|
||||
MAX(version_no)
|
||||
FROM
|
||||
nico_tag_versions
|
||||
WHERE
|
||||
tag_id = tags.id)
|
||||
ELSE
|
||||
(SELECT
|
||||
MAX(version_no)
|
||||
FROM
|
||||
tag_versions
|
||||
WHERE
|
||||
tag_id = tags.id)
|
||||
END)
|
||||
SQL
|
||||
|
||||
change_column_null :tags, :version_no, false
|
||||
|
||||
add_check_constraint :tags, 'version_no > 0', name: 'chk_tags_version_no_positive'
|
||||
end
|
||||
|
||||
def down
|
||||
remove_check_constraint :tags, name: 'chk_tags_version_no_positive'
|
||||
remove_column :tags, :version_no
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
class AddVersionNoToWikiPages < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
add_column :wiki_pages, :version_no, :integer
|
||||
|
||||
execute <<~SQL
|
||||
UPDATE
|
||||
wiki_pages
|
||||
SET
|
||||
version_no = (
|
||||
SELECT
|
||||
MAX(version_no)
|
||||
FROM
|
||||
wiki_versions
|
||||
WHERE
|
||||
wiki_page_id = wiki_pages.id)
|
||||
SQL
|
||||
|
||||
change_column_null :wiki_pages, :version_no, false
|
||||
|
||||
add_check_constraint :wiki_pages, 'version_no > 0', name: 'chk_wiki_pages_version_no_positive'
|
||||
end
|
||||
|
||||
def down
|
||||
remove_check_constraint :wiki_pages, name: 'chk_wiki_pages_version_no_positive'
|
||||
remove_column :wiki_pages, :version_no
|
||||
end
|
||||
end
|
||||
Generated
+7
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_05_01_153900) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_05_07_213300) do
|
||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -186,8 +186,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_01_153900) do
|
||||
t.datetime "original_created_from"
|
||||
t.datetime "original_created_before"
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "version_no", null: false
|
||||
t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id"
|
||||
t.index ["url"], name: "index_posts_on_url", unique: true
|
||||
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
|
||||
end
|
||||
|
||||
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
@@ -262,8 +264,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_01_153900) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "post_count", default: 0, null: false
|
||||
t.datetime "discarded_at"
|
||||
t.integer "version_no", null: false
|
||||
t.index ["discarded_at"], name: "index_tags_on_discarded_at"
|
||||
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
||||
t.check_constraint "`version_no` > 0", name: "chk_tags_version_no_positive"
|
||||
end
|
||||
|
||||
create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
@@ -369,10 +373,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_01_153900) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "discarded_at"
|
||||
t.integer "next_asset_no", default: 1, null: false
|
||||
t.integer "version_no", null: false
|
||||
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
||||
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
||||
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
|
||||
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
||||
t.check_constraint "`version_no` > 0", name: "chk_wiki_pages_version_no_positive"
|
||||
end
|
||||
|
||||
create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
|
||||
@@ -10,6 +10,10 @@ RSpec.describe 'Posts API', type: :request do
|
||||
allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true)
|
||||
end
|
||||
|
||||
def create_nico_tag!(name)
|
||||
Tag.find_or_create_by_tag_name!(name, category: :nico)
|
||||
end
|
||||
|
||||
def dummy_upload
|
||||
# 中身は何でもいい(加工処理はスタブしてる)
|
||||
Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg')
|
||||
@@ -23,21 +27,34 @@ RSpec.describe 'Posts API', type: :request do
|
||||
Post.create!(title:, url:)
|
||||
end
|
||||
|
||||
def create_post_version_for! post
|
||||
PostVersion.create!(
|
||||
post:,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
title: post.title,
|
||||
url: post.url,
|
||||
thumbnail_base: post.thumbnail_base,
|
||||
tags: post.snapshot_tag_names.join(' '),
|
||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||
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
|
||||
)
|
||||
def create_post_version_for!(post)
|
||||
version =
|
||||
PostVersion.create!(
|
||||
post:,
|
||||
version_no: 1,
|
||||
event_type: 'create',
|
||||
title: post.title,
|
||||
url: post.url,
|
||||
thumbnail_base: post.thumbnail_base,
|
||||
tags: post.snapshot_tag_names.join(' '),
|
||||
parent_post_ids: post.snapshot_parent_post_ids.join(' '),
|
||||
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)
|
||||
|
||||
post.update_columns(version_no: version.version_no) if post.has_attribute?(:version_no)
|
||||
post.version_no = version.version_no if post.respond_to?(:version_no=)
|
||||
|
||||
version
|
||||
end
|
||||
|
||||
def post_update_params(post, params = { })
|
||||
base_version =
|
||||
post.post_versions.order(version_no: :desc).first ||
|
||||
create_post_version_for!(post.reload)
|
||||
|
||||
post_write_params({ base_version_no: base_version.version_no }.merge(params))
|
||||
end
|
||||
|
||||
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
|
||||
@@ -806,24 +823,26 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
it '401 when not logged in' do
|
||||
sign_out
|
||||
put "/posts/#{post_record.id}", params: post_write_params(title: 'updated', tags: 'spec_tag')
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record, title: 'updated', tags: 'spec_tag')
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it '403 when not member' do
|
||||
sign_in_as(create(:user, role: 'guest'))
|
||||
put "/posts/#{post_record.id}", params: post_write_params(title: 'updated', tags: 'spec_tag')
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record, title: 'updated', tags: 'spec_tag')
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it '200 and updates title + resync tags when member' do
|
||||
sign_in_as(member)
|
||||
|
||||
# 追加で別タグも作って、更新時に入れ替わることを見る
|
||||
tn2 = TagName.create!(name: 'spec_tag_2')
|
||||
Tag.create!(tag_name: tn2, category: :general)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag_2')
|
||||
|
||||
@@ -831,7 +850,6 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(json).to have_key('tags')
|
||||
expect(json['tags']).to be_an(Array)
|
||||
|
||||
# show と同様、update 後レスポンスもツリー形式
|
||||
names = json['tags'].map { |n| n['name'] }
|
||||
expect(names).to include('spec_tag_2')
|
||||
end
|
||||
@@ -846,10 +864,10 @@ RSpec.describe 'Posts API', type: :request do
|
||||
it 'return 400' do
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
title: 'updated title',
|
||||
tags: 'nico:nico_tag'
|
||||
)
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'nico:nico_tag')
|
||||
|
||||
expect(response).to have_http_status(:bad_request), response.body
|
||||
end
|
||||
@@ -887,11 +905,11 @@ RSpec.describe 'Posts API', type: :request do
|
||||
it 'replaces parent posts' do
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
parent_post_ids: "#{new_parent_post_1.id} #{new_parent_post_2.id}"
|
||||
)
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
parent_post_ids: "#{new_parent_post_1.id} #{new_parent_post_2.id}")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
@@ -908,7 +926,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
it 'clears parent posts when parent_post_ids is blank' do
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
parent_post_ids: ''
|
||||
@@ -922,7 +941,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
sign_in_as(member)
|
||||
create_post_version_for!(post_record.reload)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
parent_post_ids: "#{new_parent_post_1.id} #{new_parent_post_2.id}"
|
||||
@@ -943,7 +963,10 @@ RSpec.describe 'Posts API', type: :request do
|
||||
it 'returns 422' do
|
||||
sign_in_as(member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
put "/posts/#{post_record.id}", params: {
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag' }
|
||||
|
||||
@@ -966,7 +989,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
parent_post:
|
||||
)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
parent_post_ids: 'abc'
|
||||
@@ -991,7 +1015,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
parent_post:
|
||||
)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
parent_post_ids: '999999999'
|
||||
@@ -1006,7 +1031,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
it 'returns 422 and does not create self implication' do
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
put "/posts/#{post_record.id}", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
parent_post_ids: post_record.id.to_s
|
||||
@@ -1020,6 +1046,221 @@ RSpec.describe 'Posts API', type: :request do
|
||||
)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with optimistic locking' do
|
||||
let!(:no_deerjikist_tag) { Tag.no_deerjikist }
|
||||
|
||||
before do
|
||||
PostTag.create!(post: post_record, tag: no_deerjikist_tag)
|
||||
end
|
||||
|
||||
it '400 when base_version_no is missing without force' do
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag')
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it '400 when force and merge are both true' do
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
force: '1',
|
||||
merge: '1')
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it '409 when scalar fields are changed both by current and incoming updates' do
|
||||
sign_in_as(member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
post_record.update!(title: 'updated by other user')
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated by me',
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name}")
|
||||
|
||||
expect(response).to have_http_status(:conflict)
|
||||
|
||||
expect(json.fetch('error')).to eq('conflict')
|
||||
expect(json.fetch('base_version_no')).to eq(base_version.version_no)
|
||||
expect(json.fetch('current_version_no')).to eq(2)
|
||||
expect(json.fetch('mergeable')).to be(false)
|
||||
|
||||
conflict_fields = json.fetch('conflicts').map { |change| change.fetch('field') }
|
||||
expect(conflict_fields).to include('title')
|
||||
|
||||
expect(post_record.reload.title).to eq('updated by other user')
|
||||
end
|
||||
|
||||
it 'returns 409 with mergeable true when stale tag changes do not conflict but merge is not requested' do
|
||||
sign_in_as(member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
current_tag = Tag.find_or_create_by_tag_name!('current_added_tag', category: :general)
|
||||
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
||||
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_added_tag")
|
||||
|
||||
expect(response).to have_http_status(:conflict)
|
||||
|
||||
expect(json.fetch('mergeable')).to be(true)
|
||||
|
||||
tag_change = json.fetch('changes').find { |change| change.fetch('field') == 'tag_names' }
|
||||
expect(tag_change).to be_present
|
||||
expect(tag_change.fetch('conflict')).to be(false)
|
||||
expect(tag_change.fetch('added_by_current')).to include('current_added_tag')
|
||||
expect(tag_change.fetch('added_by_me')).to include('incoming_added_tag')
|
||||
end
|
||||
|
||||
it 'merges non-conflicting stale tag changes when merge is true' do
|
||||
sign_in_as(member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
current_tag = Tag.find_or_create_by_tag_name!('current_merge_tag', category: :general)
|
||||
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
|
||||
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_merge_tag",
|
||||
merge: '1')
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).to include('current_merge_tag')
|
||||
expect(names).to include('incoming_merge_tag')
|
||||
end
|
||||
|
||||
it 'does not conflict when only nico tags changed after the base version' do
|
||||
sign_in_as(member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
nico_tag = create_nico_tag!('nico:optimistic_lock_nico')
|
||||
PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
|
||||
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
expect(post_record.reload.version_no).to eq(2)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: "spec_tag #{ Tag.no_deerjikist.name }")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).to include(nico_tag.name)
|
||||
end
|
||||
|
||||
it 'keeps nico tags even when they are not included in PUT tags' do
|
||||
sign_in_as(member)
|
||||
|
||||
nico_tag = create_nico_tag!('nico:readonly_update_nico')
|
||||
PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated title',
|
||||
tags: "spec_tag #{ Tag.no_deerjikist.name }")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).to include(nico_tag.name)
|
||||
end
|
||||
|
||||
it 'allows non-nico tags linked from nico tags to be removed by normal post update' do
|
||||
sign_in_as(member)
|
||||
|
||||
nico_tag = create_nico_tag!('nico:relation_source')
|
||||
linked_tag = Tag.find_or_create_by_tag_name!('relation_linked_tag', category: :general)
|
||||
|
||||
NicoTagRelation.create!(nico_tag:, tag: linked_tag)
|
||||
PostTag.create!(post: post_record, tag: nico_tag, created_user: member)
|
||||
PostTag.create!(post: post_record, tag: linked_tag, created_user: member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: "spec_tag #{ Tag.no_deerjikist.name }")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
names = post_record.reload.tags.map(&:name)
|
||||
|
||||
expect(names).to include(nico_tag.name)
|
||||
expect(names).to include('spec_tag')
|
||||
expect(names).to include(Tag.no_deerjikist.name)
|
||||
expect(names).not_to include(linked_tag.name)
|
||||
end
|
||||
|
||||
it 'force-updates stale posts without base_version_no' do
|
||||
sign_in_as(member)
|
||||
|
||||
create_post_version_for!(post_record.reload)
|
||||
|
||||
post_record.update!(title: 'updated by other user')
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
title: 'forced title',
|
||||
tags: "spec_tag #{Tag.no_deerjikist.name}",
|
||||
force: '1')
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(post_record.reload.title).to eq('forced title')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /posts/random' do
|
||||
@@ -1434,13 +1675,14 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
it 'creates next version on PUT /posts/:id when snapshot changes' do
|
||||
sign_in_as(member)
|
||||
create_post_version_for!(post_record)
|
||||
base_version = 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: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag_2')
|
||||
end.to change(PostVersion, :count).by(1)
|
||||
@@ -1459,13 +1701,15 @@ RSpec.describe 'Posts API', type: :request do
|
||||
sign_in_as(member)
|
||||
|
||||
PostTag.create!(post: post_record, tag: Tag.no_deerjikist)
|
||||
create_post_version_for!(post_record.reload)
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
expect {
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: post_record.title,
|
||||
tags: 'spec_tag')
|
||||
}.not_to change(PostVersion, :count)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
version = post_record.reload.post_versions.order(:version_no).last
|
||||
@@ -1490,10 +1734,11 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
it 'does not create a version when PUT /posts/:id is invalid' do
|
||||
sign_in_as(member)
|
||||
create_post_version_for!(post_record)
|
||||
base_version = create_post_version_for!(post_record)
|
||||
|
||||
expect do
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag',
|
||||
original_created_from: Time.zone.local(2020, 1, 2, 0, 0, 0).iso8601,
|
||||
@@ -1507,46 +1752,22 @@ RSpec.describe 'Posts API', type: :request do
|
||||
describe 'tag versioning from post write actions' do
|
||||
let(:member) { create(:user, :member) }
|
||||
|
||||
it 'creates tag snapshot for normalised tags on POST /posts' do
|
||||
sign_in_as(member)
|
||||
|
||||
expect {
|
||||
post '/posts', params: post_write_params(
|
||||
title: 'tag versioned post',
|
||||
url: 'https://example.com/tag-versioned-post',
|
||||
tags: 'spec_tag',
|
||||
thumbnail: dummy_upload)
|
||||
}.to change { tag.reload.tag_versions.count }.by(1)
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
version = tag.reload.tag_versions.order(:version_no).last
|
||||
expect(version.version_no).to eq(1)
|
||||
expect(version.event_type).to eq('create')
|
||||
expect(version.name).to eq('spec_tag')
|
||||
expect(version.category).to eq('general')
|
||||
expect(version.created_by_user_id).to eq(member.id)
|
||||
end
|
||||
|
||||
it 'creates tag snapshot for normalised tags on PUT /posts/:id' do
|
||||
sign_in_as(member)
|
||||
|
||||
base_version = create_post_version_for!(post_record.reload)
|
||||
|
||||
tag_name2 = TagName.create!(name: 'spec_tag_2')
|
||||
tag2 = Tag.create!(tag_name: tag_name2, category: :general)
|
||||
|
||||
expect {
|
||||
put "/posts/#{post_record.id}", params: post_write_params(
|
||||
base_version_no: base_version.version_no,
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag_2')
|
||||
}.to change { tag2.reload.tag_versions.count }.by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
version = tag2.reload.tag_versions.order(:version_no).last
|
||||
expect(version.version_no).to eq(1)
|
||||
expect(version.event_type).to eq('create')
|
||||
expect(version.name).to eq('spec_tag_2')
|
||||
expect(version.created_by_user_id).to eq(member.id)
|
||||
expect(response).to have_http_status(:ok), response.body
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,17 +26,22 @@ RSpec.describe 'TagVersions API', type: :request do
|
||||
created_by_user:,
|
||||
created_at:
|
||||
)
|
||||
TagVersion.create!(
|
||||
tag: tag,
|
||||
version_no: version_no,
|
||||
event_type: event_type,
|
||||
name: name,
|
||||
category: category,
|
||||
aliases: Array(aliases).join(' '),
|
||||
parent_tag_ids: Array(parent_tags).map(&:id).join(' '),
|
||||
created_by_user: created_by_user,
|
||||
created_at: created_at
|
||||
)
|
||||
version =
|
||||
TagVersion.create!(
|
||||
tag: tag,
|
||||
version_no: version_no,
|
||||
event_type: event_type,
|
||||
name: name,
|
||||
category: category,
|
||||
aliases: Array(aliases).join(' '),
|
||||
parent_tag_ids: Array(parent_tags).map(&:id).join(' '),
|
||||
created_by_user: created_by_user,
|
||||
created_at: created_at)
|
||||
|
||||
tag.update_columns(version_no: version_no) if tag.has_attribute?(:version_no)
|
||||
tag.version_no = version_no if tag.respond_to?(:version_no=)
|
||||
|
||||
version
|
||||
end
|
||||
|
||||
let!(:v1) do
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe VersionRecorder do
|
||||
let(:member) { create(:user, :member) }
|
||||
|
||||
let(:post_record) do
|
||||
Post.create!(
|
||||
title: 'version recorder post',
|
||||
url: 'https://example.com/version-recorder-post')
|
||||
end
|
||||
|
||||
it 'updates record version_no when creating the first version' do
|
||||
version =
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record,
|
||||
event_type: :create,
|
||||
created_by_user: member)
|
||||
|
||||
expect(version.version_no).to eq(1)
|
||||
expect(post_record.reload.version_no).to eq(1)
|
||||
end
|
||||
|
||||
it 'updates record version_no when creating the next version' do
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record,
|
||||
event_type: :create,
|
||||
created_by_user: member)
|
||||
|
||||
post_record.update!(title: 'updated version recorder post')
|
||||
|
||||
version =
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
expect(version.version_no).to eq(2)
|
||||
expect(post_record.reload.version_no).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not create a new version or advance version_no when snapshot is unchanged' do
|
||||
first =
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record,
|
||||
event_type: :create,
|
||||
created_by_user: member)
|
||||
|
||||
expect {
|
||||
version =
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
expect(version).to eq(first)
|
||||
}.not_to change(PostVersion, :count)
|
||||
|
||||
expect(post_record.reload.version_no).to eq(1)
|
||||
end
|
||||
|
||||
it 'raises when record version_no is older than the latest version' do
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record,
|
||||
event_type: :create,
|
||||
created_by_user: member)
|
||||
|
||||
post_record.update!(title: 'updated once')
|
||||
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
|
||||
post_record.update_columns(version_no: 1)
|
||||
|
||||
post_record.update!(title: 'updated with stale version_no')
|
||||
|
||||
expect {
|
||||
PostVersionRecorder.record!(
|
||||
post: post_record.reload,
|
||||
event_type: :update,
|
||||
created_by_user: member)
|
||||
}.to raise_error(RuntimeError, /version_no/)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user