mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 10:58:31 -05:00
e7f251c105
FIX: history revision can now properly be hidden FIX: PostRevision serializer is now entirely dynamic to properly handle hidden revisions FIX: default history modal to "side by side" view on mobile FIX: properly hiden which revision has been hidden UX: inline category/user/wiki/post_type changes with the revision details FEATURE: new '/posts/:post_id/revisions/latest' endpoint to retrieve latest revision UX: do not show the hide/show revision button on mobile (no room for them) UX: remove CSS transitions on the buttons in the history modal FIX: PostRevisor now handles all the changes that might create new revisions FIX: PostRevision.ensure_consistency! was wrong due to off by 1 mistake... refactored topic's callbacks for better readability extracted 'PostRevisionGuardian'
308 lines
7.9 KiB
Ruby
308 lines
7.9 KiB
Ruby
require "edit_rate_limiter"
|
|
|
|
class PostRevisor
|
|
|
|
POST_TRACKED_FIELDS = %w{raw cooked edit_reason user_id wiki post_type}
|
|
TOPIC_TRACKED_FIELDS = %w{title category_id}
|
|
|
|
attr_reader :category_changed
|
|
|
|
def initialize(post, topic=nil)
|
|
@post = post
|
|
@topic = topic || post.topic
|
|
end
|
|
|
|
# AVAILABLE OPTIONS:
|
|
# - revised_at: changes the date of the revision
|
|
# - force_new_version: bypass ninja-edit window
|
|
# - bypass_rate_limiter:
|
|
# - bypass_bump: do not bump the topic, even if last post
|
|
# - skip_validations: ask ActiveRecord to skip validations
|
|
def revise!(editor, fields, opts={})
|
|
@editor = editor
|
|
@fields = fields.with_indifferent_access
|
|
@opts = opts
|
|
|
|
# some normalization
|
|
@fields[:raw] = cleanup_whitespaces(@fields[:raw]) if @fields.has_key?(:raw)
|
|
@fields[:user_id] = @fields[:user_id].to_i if @fields.has_key?(:user_id)
|
|
@fields[:category_id] = @fields[:category_id].to_i if @fields.has_key?(:category_id)
|
|
|
|
# always reset edit_reason unless provided
|
|
@fields[:edit_reason] = nil unless @fields.has_key?(:edit_reason)
|
|
|
|
return false unless should_revise?
|
|
|
|
@post.acting_user = @editor
|
|
@topic.acting_user = @editor
|
|
@revised_at = @opts[:revised_at] || Time.now
|
|
@last_version_at = @post.last_version_at || Time.now
|
|
|
|
@version_changed = false
|
|
@post_successfully_saved = true
|
|
@topic_successfully_saved = true
|
|
|
|
Post.transaction do
|
|
revise_post
|
|
|
|
# TODO: these callbacks are being called in a transaction
|
|
# it is kind of odd, because the callback is called "before_edit"
|
|
# but the post is already edited at this point
|
|
# Trouble is that much of the logic of should I edit? is deeper
|
|
# down so yanking this in front of the transaction will lead to
|
|
# false positive.
|
|
plugin_callbacks
|
|
|
|
revise_topic
|
|
advance_draft_sequence
|
|
end
|
|
|
|
# WARNING: do not pull this into the transaction
|
|
# it can fire events in sidekiq before the post is done saving
|
|
# leading to corrupt state
|
|
post_process_post
|
|
|
|
update_topic_word_counts
|
|
alert_users
|
|
publish_changes
|
|
grant_badge
|
|
|
|
@post_successfully_saved && @topic_successfully_saved
|
|
end
|
|
|
|
def cleanup_whitespaces(raw)
|
|
TextCleaner.normalize_whitespaces(raw).gsub(/\s+\z/, "")
|
|
end
|
|
|
|
def should_revise?
|
|
post_changed? || topic_changed?
|
|
end
|
|
|
|
def post_changed?
|
|
POST_TRACKED_FIELDS.each do |field|
|
|
return true if @fields.has_key?(field) && @fields[field] != @post.send(field)
|
|
end
|
|
false
|
|
end
|
|
|
|
def topic_changed?
|
|
TOPIC_TRACKED_FIELDS.each do |field|
|
|
return true if @fields.has_key?(field) && @fields[field] != @topic.send(field)
|
|
end
|
|
false
|
|
end
|
|
|
|
def revise_post
|
|
if should_create_new_version?
|
|
revise_and_create_new_version
|
|
else
|
|
revise
|
|
end
|
|
end
|
|
|
|
def should_create_new_version?
|
|
edited_by_another_user? || !ninja_edit? || owner_changed? || force_new_version?
|
|
end
|
|
|
|
def edited_by_another_user?
|
|
@post.last_editor_id != @editor.id
|
|
end
|
|
|
|
def ninja_edit?
|
|
@revised_at - @last_version_at <= SiteSetting.ninja_edit_window.to_i
|
|
end
|
|
|
|
def owner_changed?
|
|
@fields.has_key?(:user_id) && @fields[:user_id] != @post.user_id
|
|
end
|
|
|
|
def force_new_version?
|
|
@opts[:force_new_version] == true
|
|
end
|
|
|
|
def revise_and_create_new_version
|
|
@version_changed = true
|
|
@post.version += 1
|
|
@post.public_version += 1
|
|
@post.last_version_at = @revised_at
|
|
|
|
revise
|
|
perform_edit
|
|
bump_topic
|
|
end
|
|
|
|
def revise
|
|
update_post
|
|
update_topic if topic_changed?
|
|
create_or_update_revision
|
|
end
|
|
|
|
def update_post
|
|
POST_TRACKED_FIELDS.each do |field|
|
|
@post.send("#{field}=", @fields[field]) if @fields.has_key?(field)
|
|
end
|
|
|
|
@post.last_editor_id = @editor.id
|
|
@post.word_count = @fields[:raw].scan(/\w+/).size if @fields.has_key?(:raw)
|
|
@post.self_edits += 1 if self_edit?
|
|
|
|
clear_flags_and_unhide_post
|
|
|
|
@post.extract_quoted_post_numbers
|
|
@post_successfully_saved = @post.save(validate: !@opts[:skip_validations])
|
|
@post.save_reply_relationships
|
|
end
|
|
|
|
def self_edit?
|
|
@editor == @post.user
|
|
end
|
|
|
|
def clear_flags_and_unhide_post
|
|
return unless editing_a_flagged_and_hidden_post?
|
|
PostAction.clear_flags!(@post, Discourse.system_user)
|
|
@post.unhide!
|
|
end
|
|
|
|
def editing_a_flagged_and_hidden_post?
|
|
self_edit? &&
|
|
@post.hidden &&
|
|
@post.hidden_reason_id == Post.hidden_reasons[:flag_threshold_reached]
|
|
end
|
|
|
|
def update_topic
|
|
@topic.title = @fields[:title] if @fields.has_key?(:title)
|
|
Topic.transaction do
|
|
@topic_successfully_saved = @topic.change_category_to_id(@fields[:category_id]) if @fields.has_key?(:category_id)
|
|
@topic_successfully_saved &&= @topic.save(validate: !@opts[:skip_validations])
|
|
end
|
|
end
|
|
|
|
def create_or_update_revision
|
|
if @version_changed
|
|
create_revision
|
|
else
|
|
update_revision
|
|
end
|
|
end
|
|
|
|
def create_revision
|
|
modifications = post_changes.merge(topic_changes)
|
|
PostRevision.create!(
|
|
user_id: @post.last_editor_id,
|
|
post_id: @post.id,
|
|
number: @post.version,
|
|
modifications: modifications
|
|
)
|
|
end
|
|
|
|
def update_revision
|
|
return unless revision = PostRevision.find_by(post_id: @post.id, number: @post.version)
|
|
revision.user_id = @post.last_editor_id
|
|
modifications = post_changes.merge(topic_changes)
|
|
modifications.keys.each do |field|
|
|
if revision.modifications.has_key?(field)
|
|
old_value = revision.modifications[field][0]
|
|
new_value = modifications[field][1]
|
|
revision.modifications[field] = [old_value, new_value]
|
|
else
|
|
revision.modifications[field] = modifications[field]
|
|
end
|
|
end
|
|
revision.save
|
|
end
|
|
|
|
def post_changes
|
|
@post.previous_changes.slice(*POST_TRACKED_FIELDS)
|
|
end
|
|
|
|
def topic_changes
|
|
@topic.previous_changes.slice(*TOPIC_TRACKED_FIELDS)
|
|
end
|
|
|
|
def perform_edit
|
|
return if bypass_rate_limiter?
|
|
EditRateLimiter.new(@editor).performed!
|
|
end
|
|
|
|
def bypass_rate_limiter?
|
|
@opts[:bypass_rate_limiter] == true
|
|
end
|
|
|
|
def bump_topic
|
|
return if bypass_bump? || !is_last_post?
|
|
@topic.update_column(:bumped_at, Time.now)
|
|
TopicTrackingState.publish_latest(@topic)
|
|
end
|
|
|
|
def bypass_bump?
|
|
@opts[:bypass_bump] == true
|
|
end
|
|
|
|
def is_last_post?
|
|
!Post.where(topic_id: @topic.id)
|
|
.where("post_number > ?", @post.post_number)
|
|
.exists?
|
|
end
|
|
|
|
def plugin_callbacks
|
|
DiscourseEvent.trigger(:before_edit_post, @post)
|
|
DiscourseEvent.trigger(:validate_post, @post)
|
|
end
|
|
|
|
def revise_topic
|
|
return unless @post.post_number == 1
|
|
|
|
update_topic_excerpt
|
|
update_category_description
|
|
end
|
|
|
|
def update_topic_excerpt
|
|
excerpt = @post.excerpt(220, strip_links: true)
|
|
@topic.update_column(:excerpt, excerpt)
|
|
end
|
|
|
|
def update_category_description
|
|
return unless category = Category.find_by(topic_id: @topic.id)
|
|
|
|
body = @post.cooked
|
|
matches = body.scan(/\<p\>(.*)\<\/p\>/)
|
|
if matches && matches[0] && matches[0][0]
|
|
new_description = matches[0][0]
|
|
new_description = nil if new_description == I18n.t("category.replace_paragraph")
|
|
category.update_column(:description, new_description)
|
|
@category_changed = category
|
|
end
|
|
end
|
|
|
|
def advance_draft_sequence
|
|
@post.advance_draft_sequence
|
|
end
|
|
|
|
def post_process_post
|
|
@post.invalidate_oneboxes = true
|
|
@post.trigger_post_process
|
|
end
|
|
|
|
def update_topic_word_counts
|
|
Topic.exec_sql("UPDATE topics
|
|
SET word_count = (
|
|
SELECT SUM(COALESCE(posts.word_count, 0))
|
|
FROM posts
|
|
WHERE posts.topic_id = :topic_id
|
|
)
|
|
WHERE topics.id = :topic_id", topic_id: @topic.id)
|
|
end
|
|
|
|
def alert_users
|
|
PostAlerter.new.after_save_post(@post)
|
|
end
|
|
|
|
def publish_changes
|
|
@post.publish_change_to_clients!(:revised)
|
|
end
|
|
|
|
def grant_badge
|
|
BadgeGranter.queue_badge_grant(Badge::Trigger::PostRevision, post: @post)
|
|
end
|
|
|
|
end
|