|
|
|
@@ -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
|
|
|
|
|