FEATURE: revert post to a specific revision
This commit is contained in:
parent
a212540779
commit
89248580dc
|
@ -29,6 +29,25 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
Discourse.Post.showRevision(postId, postVersion).then(() => this.refresh(postId, postVersion));
|
Discourse.Post.showRevision(postId, postVersion).then(() => this.refresh(postId, postVersion));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
revert(post, postVersion) {
|
||||||
|
post.revertToRevision(postVersion).then((result) => {
|
||||||
|
this.refresh(post.get('id'), postVersion);
|
||||||
|
if (result.topic) {
|
||||||
|
post.set('topic.slug', result.topic.slug);
|
||||||
|
post.set('topic.title', result.topic.title);
|
||||||
|
post.set('topic.fancy_title', result.topic.fancy_title);
|
||||||
|
}
|
||||||
|
if (result.category_id) {
|
||||||
|
post.set('topic.category', Discourse.Category.findById(result.category_id));
|
||||||
|
}
|
||||||
|
this.send("closeModal");
|
||||||
|
}).catch(function(e) {
|
||||||
|
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors && e.jqXHR.responseJSON.errors[0]) {
|
||||||
|
bootbox.alert(e.jqXHR.responseJSON.errors[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
@computed('model.created_at')
|
@computed('model.created_at')
|
||||||
createdAtDate(createdAt) {
|
createdAtDate(createdAt) {
|
||||||
return moment(createdAt).format("LLLL");
|
return moment(createdAt).format("LLLL");
|
||||||
|
@ -69,6 +88,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
return !prevHidden && this.currentUser && this.currentUser.get('staff');
|
return !prevHidden && this.currentUser && this.currentUser.get('staff');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed()
|
||||||
|
displayRevert() {
|
||||||
|
return this.currentUser && this.currentUser.get('staff');
|
||||||
|
},
|
||||||
|
|
||||||
isEitherRevisionHidden: Ember.computed.or("model.previous_hidden", "model.current_hidden"),
|
isEitherRevisionHidden: Ember.computed.or("model.previous_hidden", "model.current_hidden"),
|
||||||
|
|
||||||
@computed('model.previous_hidden', 'model.current_hidden', 'displayingInline')
|
@computed('model.previous_hidden', 'model.current_hidden', 'displayingInline')
|
||||||
|
@ -142,6 +166,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
hideVersion() { this.hide(this.get("model.post_id"), this.get("model.current_revision")); },
|
hideVersion() { this.hide(this.get("model.post_id"), this.get("model.current_revision")); },
|
||||||
showVersion() { this.show(this.get("model.post_id"), this.get("model.current_revision")); },
|
showVersion() { this.show(this.get("model.post_id"), this.get("model.current_revision")); },
|
||||||
|
|
||||||
|
revertToVersion() { this.revert(this.get("post"), this.get("model.current_revision")); },
|
||||||
|
|
||||||
displayInline() { this.set("viewMode", "inline"); },
|
displayInline() { this.set("viewMode", "inline"); },
|
||||||
displaySideBySide() { this.set("viewMode", "side_by_side"); },
|
displaySideBySide() { this.set("viewMode", "side_by_side"); },
|
||||||
displaySideBySideMarkdown() { this.set("viewMode", "side_by_side_markdown"); }
|
displaySideBySideMarkdown() { this.set("viewMode", "side_by_side_markdown"); }
|
||||||
|
|
|
@ -271,6 +271,10 @@ const Post = RestModel.extend({
|
||||||
json = Post.munge(json);
|
json = Post.munge(json);
|
||||||
this.set('actions_summary', json.actions_summary);
|
this.set('actions_summary', json.actions_summary);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
revertToRevision(version) {
|
||||||
|
return Discourse.ajax(`/posts/${this.get('id')}/revisions/${version}/revert`, { type: 'PUT' });
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -73,6 +73,7 @@ const TopicRoute = Discourse.Route.extend({
|
||||||
showHistory(model) {
|
showHistory(model) {
|
||||||
showModal('history', { model });
|
showModal('history', { model });
|
||||||
this.controllerFor('history').refresh(model.get("id"), "latest");
|
this.controllerFor('history').refresh(model.get("id"), "latest");
|
||||||
|
this.controllerFor('history').set('post', model);
|
||||||
this.controllerFor('modal').set('modalClass', 'history-modal');
|
this.controllerFor('modal').set('modalClass', 'history-modal');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,6 @@
|
||||||
</div>
|
</div>
|
||||||
{{d-button action="loadNextVersion" icon="forward" title="post.revisions.controls.next" disabled=loadNextDisabled}}
|
{{d-button action="loadNextVersion" icon="forward" title="post.revisions.controls.next" disabled=loadNextDisabled}}
|
||||||
{{d-button action="loadLastVersion" icon="fast-forward" title="post.revisions.controls.last" disabled=loadLastDisabled}}
|
{{d-button action="loadLastVersion" icon="fast-forward" title="post.revisions.controls.last" disabled=loadLastDisabled}}
|
||||||
{{#if displayHide}}
|
|
||||||
{{d-button action="hideVersion" icon="trash-o" title="post.revisions.controls.hide" class="btn-danger" disabled=loading}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if displayShow}}
|
|
||||||
{{d-button action="showVersion" icon="undo" title="post.revisions.controls.show" disabled=loading}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
<div id="display-modes">
|
<div id="display-modes">
|
||||||
{{d-button action="displayInline" label="post.revisions.displays.inline.button" title="post.revisions.displays.inline.title" class=inlineClass}}
|
{{d-button action="displayInline" label="post.revisions.displays.inline.button" title="post.revisions.displays.inline.title" class=inlineClass}}
|
||||||
|
@ -85,5 +79,15 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{{bodyDiff}}}
|
{{{bodyDiff}}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if displayRevert}}
|
||||||
|
{{d-button action="revertToVersion" icon="undo" label="post.revisions.controls.revert" class="btn-danger" disabled=loading}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if displayHide}}
|
||||||
|
{{d-button action="hideVersion" icon="eye-slash" label="post.revisions.controls.hide" class="btn-danger" disabled=loading}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if displayShow}}
|
||||||
|
{{d-button action="showVersion" icon="eye" label="post.revisions.controls.show" disabled=loading}}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -282,6 +282,55 @@ class PostsController < ApplicationController
|
||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def revert
|
||||||
|
raise Discourse::NotFound unless guardian.is_staff?
|
||||||
|
|
||||||
|
post_id = params[:id] || params[:post_id]
|
||||||
|
revision = params[:revision].to_i
|
||||||
|
raise Discourse::InvalidParameters.new(:revision) if revision < 2
|
||||||
|
|
||||||
|
post_revision = PostRevision.find_by(post_id: post_id, number: revision)
|
||||||
|
raise Discourse::NotFound unless post_revision
|
||||||
|
|
||||||
|
post = find_post_from_params
|
||||||
|
raise Discourse::NotFound if post.blank?
|
||||||
|
|
||||||
|
post_revision.post = post
|
||||||
|
guardian.ensure_can_see!(post_revision)
|
||||||
|
guardian.ensure_can_edit!(post)
|
||||||
|
return render_json_error(I18n.t('revert_version_same')) if post_revision.modifications["raw"].blank? && post_revision.modifications["title"].blank? && post_revision.modifications["category_id"].blank?
|
||||||
|
|
||||||
|
topic = Topic.with_deleted.find(post.topic_id)
|
||||||
|
|
||||||
|
changes = {}
|
||||||
|
changes[:raw] = post_revision.modifications["raw"][0] if post_revision.modifications["raw"].present? && post_revision.modifications["raw"][0] != post.raw
|
||||||
|
if post.is_first_post?
|
||||||
|
changes[:title] = post_revision.modifications["title"][0] if post_revision.modifications["title"].present? && post_revision.modifications["title"][0] != topic.title
|
||||||
|
changes[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present? && post_revision.modifications["category_id"][0] != topic.category.id
|
||||||
|
end
|
||||||
|
return render_json_error(I18n.t('revert_version_same')) unless changes.length > 0
|
||||||
|
changes[:edit_reason] = "reverted to version ##{post_revision.number.to_i - 1}"
|
||||||
|
|
||||||
|
revisor = PostRevisor.new(post, topic)
|
||||||
|
revisor.revise!(current_user, changes)
|
||||||
|
|
||||||
|
return render_json_error(post) if post.errors.present?
|
||||||
|
return render_json_error(topic) if topic.errors.present?
|
||||||
|
|
||||||
|
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
|
||||||
|
post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key)
|
||||||
|
link_counts = TopicLink.counts_for(guardian, topic, [post])
|
||||||
|
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
|
||||||
|
|
||||||
|
result = { post: post_serializer.as_json }
|
||||||
|
if post.is_first_post?
|
||||||
|
result[:topic] = BasicTopicSerializer.new(topic, scope: guardian, root: false).as_json if post_revision.modifications["title"].present?
|
||||||
|
result[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
render_json_dump(result)
|
||||||
|
end
|
||||||
|
|
||||||
def bookmark
|
def bookmark
|
||||||
post = find_post_from_params
|
post = find_post_from_params
|
||||||
|
|
||||||
|
|
|
@ -1639,6 +1639,7 @@ en:
|
||||||
last: "Last revision"
|
last: "Last revision"
|
||||||
hide: "Hide revision"
|
hide: "Hide revision"
|
||||||
show: "Show revision"
|
show: "Show revision"
|
||||||
|
revert: "Revert to this revision"
|
||||||
comparing_previous_to_current_out_of_total: "<strong>{{previous}}</strong> <i class='fa fa-arrows-h'></i> <strong>{{current}}</strong> / {{total}}"
|
comparing_previous_to_current_out_of_total: "<strong>{{previous}}</strong> <i class='fa fa-arrows-h'></i> <strong>{{current}}</strong> / {{total}}"
|
||||||
displays:
|
displays:
|
||||||
inline:
|
inline:
|
||||||
|
|
|
@ -204,6 +204,7 @@ en:
|
||||||
top: "Top topics"
|
top: "Top topics"
|
||||||
posts: "Latest posts"
|
posts: "Latest posts"
|
||||||
too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted."
|
too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted."
|
||||||
|
revert_version_same: "The current version is same as the version you are trying to revert to."
|
||||||
|
|
||||||
excerpt_image: "image"
|
excerpt_image: "image"
|
||||||
|
|
||||||
|
|
|
@ -386,6 +386,7 @@ Discourse::Application.routes.draw do
|
||||||
get "revisions/:revision" => "posts#revisions", constraints: { revision: /\d+/ }
|
get "revisions/:revision" => "posts#revisions", constraints: { revision: /\d+/ }
|
||||||
put "revisions/:revision/hide" => "posts#hide_revision", constraints: { revision: /\d+/ }
|
put "revisions/:revision/hide" => "posts#hide_revision", constraints: { revision: /\d+/ }
|
||||||
put "revisions/:revision/show" => "posts#show_revision", constraints: { revision: /\d+/ }
|
put "revisions/:revision/show" => "posts#show_revision", constraints: { revision: /\d+/ }
|
||||||
|
put "revisions/:revision/revert" => "posts#revert", constraints: { revision: /\d+/ }
|
||||||
put "recover"
|
put "recover"
|
||||||
collection do
|
collection do
|
||||||
delete "destroy_many"
|
delete "destroy_many"
|
||||||
|
|
|
@ -830,6 +830,79 @@ describe PostsController do
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'revert post to a specific revision' do
|
||||||
|
include_examples 'action requires login', :put, :revert, post_id: 123, revision: 2
|
||||||
|
|
||||||
|
let(:post) { Fabricate(:post, user: logged_in_as, raw: "Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex") }
|
||||||
|
let(:post_revision) { Fabricate(:post_revision, post: post, modifications: {"raw" => ["this is original post body.", "this is edited post body."]}) }
|
||||||
|
let(:blank_post_revision) { Fabricate(:post_revision, post: post, modifications: {"edit_reason" => ["edit reason #1", "edit reason #2"]}) }
|
||||||
|
let(:same_post_revision) { Fabricate(:post_revision, post: post, modifications: {"raw" => ["Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex", "this is edited post body."]}) }
|
||||||
|
|
||||||
|
let(:revert_params) do
|
||||||
|
{
|
||||||
|
post_id: post.id,
|
||||||
|
revision: post_revision.number
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:moderator) { Fabricate(:moderator) }
|
||||||
|
|
||||||
|
describe 'when logged in as a regular user' do
|
||||||
|
let(:logged_in_as) { log_in }
|
||||||
|
|
||||||
|
it "does not work" do
|
||||||
|
xhr :put, :revert, revert_params
|
||||||
|
expect(response).to_not be_success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when logged in as staff" do
|
||||||
|
let(:logged_in_as) { log_in(:moderator) }
|
||||||
|
|
||||||
|
it "throws an exception when revision is < 2" do
|
||||||
|
expect {
|
||||||
|
xhr :put, :revert, post_id: post.id, revision: 1
|
||||||
|
}.to raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails when post_revision record is not found" do
|
||||||
|
xhr :put, :revert, post_id: post.id, revision: post_revision.number + 1
|
||||||
|
expect(response).to_not be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails when post record is not found" do
|
||||||
|
xhr :put, :revert, post_id: post.id + 1, revision: post_revision.number
|
||||||
|
expect(response).to_not be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails when revision is blank" do
|
||||||
|
xhr :put, :revert, post_id: post.id, revision: blank_post_revision.number
|
||||||
|
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(JSON.parse(response.body)['errors']).to include(I18n.t('revert_version_same'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails when revised version is same as current version" do
|
||||||
|
xhr :put, :revert, post_id: post.id, revision: same_post_revision.number
|
||||||
|
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(JSON.parse(response.body)['errors']).to include(I18n.t('revert_version_same'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "works!" do
|
||||||
|
xhr :put, :revert, revert_params
|
||||||
|
expect(response).to be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "supports reverting posts in deleted topics" do
|
||||||
|
first_post = post.topic.ordered_posts.first
|
||||||
|
PostDestroyer.new(moderator, first_post).destroy
|
||||||
|
|
||||||
|
xhr :put, :revert, revert_params
|
||||||
|
expect(response).to be_success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'expandable embedded posts' do
|
describe 'expandable embedded posts' do
|
||||||
let(:post) { Fabricate(:post) }
|
let(:post) { Fabricate(:post) }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue