This commit is contained in:
2026-05-08 02:08:18 +09:00
parent d54e66a114
commit 772c66aa64
6 changed files with 337 additions and 13 deletions
+204 -2
View File
@@ -173,15 +173,41 @@ class PostsController < ApplicationController
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
base_version_no = parse_base_version_no
force = truthy_param?(params[:force])
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
post = Post.find(params[:id].to_i)
base_version = post.post_versions.find_by!(version_no: base_version_no)
base_snapshot = post_snapshot_from_version(base_version)
current_snapshot = post_snapshot_from_record(post)
incoming_snapshot = post_incoming_snapshot(post,
title:,
original_created_from:,
original_created_before:,
tag_names:,
parent_post_ids:)
if !(force) && post.version_no != base_version_no
conflict_json = post_conflict_json(post:,
base_version_no:,
base_snapshot:,
current_snapshot:,
incoming_snapshot:)
raise ActiveRecord::Rollback
end
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
post.update!(title:, original_created_from:, original_created_before:)
@@ -198,8 +224,10 @@ class PostsController < ApplicationController
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
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
@@ -404,4 +432,178 @@ 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)
raise ArgumentError, 'base_version_no は必須です.' unless version_no&.positive?
version_no
end
def truthy_param?(value) = ActiveModel::Type::Boolean.new.cast(value)
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: version.tags.to_s.split.sort,
parent_post_ids: snapshot_parent_post_ids_from_version(version) }
end
def post_snapshot_form_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: post.tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name'),
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
end
def post_incoming_snapshot post, 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(post, 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 post, raw_tag_names
manual_names = normalised_manual_tag_names_for_snapshot(raw_tag_names)
nico_names = post.tags.nico.joins(:tag_name).pluck('tag_names.name')
existing_tags =
Tag
.joins(:tag_name)
.where(tag_names: { name: manual_names + nico_names })
.to_a
expanded_names = Tag.expand_parent_tags(existing_tags).map(&:name)
(manual_names + nico_names + expanded_names).uniq.sort
end
def normalised_manual_tag_names_for_snapshot raw_tag_names
if raw_tag_names.any? { |name| name.downcase.start_with?('nico:') }
raise Tag::NicoTagNormalisationError
end
pairs = raw_tag_names.map do |raw_name|
prefix, category =
Tag::CATEGORY_PREFIXES.find { |p, _| raw_name.downcase.start_with?(p) } || ['', nil]
name = TagName.canonicalise(raw_name.sub(/\A#{ Regexp.escape(prefix) }/i, '')).first
[name, category]
end
names = pairs.map(&:first)
has_deerjikist = pairs.any? do |name, category|
category == :deerjikist ||
Tag.joins(:tag_name).where(category: :deerjikist, tag_names: { name: }).exists?
end
names << Tag.no_deerjikist.name unless has_deerjikist
names.uniq.sort
end
def post_conflict_json post:, base_version_no:, base_snapshot:,
current_snapshot:, incoming_snapshot:
changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot)
conflicts = changes.select { |change| change[:conflict] }
{ 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
end