Add post_edit_time_limit site setting to limit the how long a post can be edited and deleted by the author. Default is 1 year.

This commit is contained in:
Neil Lalonde
2014-01-07 10:32:09 -05:00
parent e750ea010f
commit 259295d865
10 changed files with 118 additions and 5 deletions

View File

@ -459,7 +459,17 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
} }
]); ]);
} else { } else {
post.destroy(user); post.destroy(user).then(null, function(e) {
console.log('Error case?');
console.log(e);
post.undoDeleteState();
var response = $.parseJSON(e.responseText);
if (response && response.errors) {
bootbox.alert(response.errors[0]);
} else {
bootbox.alert(I18n.t('generic_error'));
}
});
} }
}, },

View File

@ -153,6 +153,7 @@ Discourse.Post = Discourse.Model.extend({
// We're updating a post // We're updating a post
return Discourse.ajax("/posts/" + (this.get('id')), { return Discourse.ajax("/posts/" + (this.get('id')), {
type: 'PUT', type: 'PUT',
dataType: 'json',
data: { data: {
post: { raw: this.get('raw'), edit_reason: this.get('editReason') }, post: { raw: this.get('raw'), edit_reason: this.get('editReason') },
image_sizes: this.get('imageSizes') image_sizes: this.get('imageSizes')
@ -236,6 +237,8 @@ Discourse.Post = Discourse.Model.extend({
@param {Discourse.User} deletedBy The user deleting the post @param {Discourse.User} deletedBy The user deleting the post
**/ **/
setDeletedState: function(deletedBy) { setDeletedState: function(deletedBy) {
this.set('oldCooked', this.get('cooked'));
// Moderators can delete posts. Regular users can only trigger a deleted at message. // Moderators can delete posts. Regular users can only trigger a deleted at message.
if (deletedBy.get('staff')) { if (deletedBy.get('staff')) {
this.setProperties({ this.setProperties({
@ -255,6 +258,26 @@ Discourse.Post = Discourse.Model.extend({
} }
}, },
/**
Changes the state of the post to NOT be deleted. Does not call the server.
This can only be called after setDeletedState was called, but the delete
failed on the server.
@method undoDeletedState
**/
undoDeleteState: function() {
if (this.get('oldCooked')) {
this.setProperties({
deleted_at: null,
deleted_by: null,
cooked: this.get('oldCooked'),
version: this.get('version') - 1,
can_recover: false,
user_deleted: false
});
}
},
/** /**
Deletes a post Deletes a post

View File

@ -80,15 +80,18 @@ class ApplicationController < ActionController::Base
end end
rescue_from Discourse::NotFound do rescue_from Discourse::NotFound do
rescue_discourse_actions("[error: 'not found']", 404) rescue_discourse_actions("[error: 'not found']", 404) # TODO: this breaks json responses
end end
rescue_from Discourse::InvalidAccess do rescue_from Discourse::InvalidAccess do
rescue_discourse_actions("[error: 'invalid access']", 403) rescue_discourse_actions("[error: 'invalid access']", 403) # TODO: this breaks json responses
end end
def rescue_discourse_actions(message, error) def rescue_discourse_actions(message, error)
if request.format && request.format.json? if request.format && request.format.json?
# TODO: this doesn't make sense. Stuffing an html page into a json response will cause
# $.parseJSON to fail in the browser. Also returning text like "[error: 'invalid access']"
# from the above rescue_from blocks will fail because that isn't valid json.
render status: error, layout: false, text: (error == 404) ? build_not_found_page(error) : message render status: error, layout: false, text: (error == 404) ? build_not_found_page(error) : message
else else
render text: build_not_found_page(error, 'no_js') render text: build_not_found_page(error, 'no_js')

View File

@ -63,6 +63,12 @@ class PostsController < ApplicationController
post = post.with_deleted if guardian.is_staff? post = post.with_deleted if guardian.is_staff?
post = post.first post = post.first
post.image_sizes = params[:image_sizes] if params[:image_sizes].present? post.image_sizes = params[:image_sizes] if params[:image_sizes].present?
if !guardian.can_edit?(post) && post.user_id == current_user.id && post.edit_time_limit_expired?
render json: {errors: [I18n.t('too_late_to_edit')]}, status: 422
return
end
guardian.ensure_can_edit!(post) guardian.ensure_can_edit!(post)
# to stay consistent with the create api, # to stay consistent with the create api,
@ -127,6 +133,12 @@ class PostsController < ApplicationController
def destroy def destroy
post = find_post_from_params post = find_post_from_params
if !guardian.can_delete_post?(post) && post.user_id == current_user.id && post.edit_time_limit_expired?
render json: {errors: [I18n.t('too_late_to_edit')]}, status: 422
return
end
guardian.ensure_can_delete!(post) guardian.ensure_can_delete!(post)
destroyer = PostDestroyer.new(current_user, post) destroyer = PostDestroyer.new(current_user, post)

View File

@ -413,6 +413,14 @@ class Post < ActiveRecord::Base
end end
end end
def edit_time_limit_expired?
if created_at && SiteSetting.post_edit_time_limit.to_i > 0
created_at < SiteSetting.post_edit_time_limit.to_i.minutes.ago
else
false
end
end
private private
def parse_quote_into_arguments(quote) def parse_quote_into_arguments(quote)

View File

@ -85,6 +85,7 @@ en:
rss_description: rss_description:
latest: "Latest topics" latest: "Latest topics"
hot: "Hot topics" hot: "Hot topics"
too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted."
groups: groups:
errors: errors:
@ -544,6 +545,7 @@ en:
download_remote_images_to_local: "Download a copy of remote images hotlinked in posts" download_remote_images_to_local: "Download a copy of remote images hotlinked in posts"
download_remote_images_threshold: "Amount of minimum available disk space required to download remote images locally (in percent)" download_remote_images_threshold: "Amount of minimum available disk space required to download remote images locally (in percent)"
ninja_edit_window: "Number of seconds after posting where edits do not create a new version" ninja_edit_window: "Number of seconds after posting where edits do not create a new version"
post_edit_time_limit: "Amount of time in minutes in which posts can be edited and deleted by the author. Set to 0 to allow editing and deleting posts at any time."
edit_history_visible_to_public: "Allow everyone to see previous versions of an edited post. When disabled, only staff members can view edit history." edit_history_visible_to_public: "Allow everyone to see previous versions of an edited post. When disabled, only staff members can view edit history."
delete_removed_posts_after: "Number of hours after which posts removed by the author will be deleted." delete_removed_posts_after: "Number of hours after which posts removed by the author will be deleted."
max_image_width: "Maximum allowed width of images in a post" max_image_width: "Maximum allowed width of images in a post"

View File

@ -164,6 +164,7 @@ posting:
default: 15 default: 15
enable_private_messages: true enable_private_messages: true
ninja_edit_window: 300 ninja_edit_window: 300
post_edit_time_limit: 525600
edit_history_visible_to_public: edit_history_visible_to_public:
client: true client: true
default: true default: true

View File

@ -276,7 +276,7 @@ class Guardian
end end
def can_edit_post?(post) def can_edit_post?(post)
is_staff? || (!post.topic.archived? && is_my_own?(post) && !post.user_deleted &&!post.deleted_at) is_staff? || (!post.topic.archived? && is_my_own?(post) && !post.user_deleted && !post.deleted_at && !post.edit_time_limit_expired?)
end end
def can_edit_user?(user) def can_edit_user?(user)
@ -304,6 +304,9 @@ class Guardian
# Can't delete the first post # Can't delete the first post
return false if post.post_number == 1 return false if post.post_number == 1
# Can't delete after post_edit_time_limit minutes have passed
return false if !is_staff? && post.edit_time_limit_expired?
# You can delete your own posts # You can delete your own posts
return !post.user_deleted? if is_my_own?(post) return !post.user_deleted? if is_my_own?(post)

View File

@ -544,6 +544,29 @@ describe Guardian do
it 'returns true as an admin' do it 'returns true as an admin' do
Guardian.new(admin).can_edit?(post).should be_true Guardian.new(admin).can_edit?(post).should be_true
end end
context 'post is older than post_edit_time_limit' do
let(:old_post) { build(:post, topic: topic, user: topic.user, created_at: 6.minutes.ago) }
before do
SiteSetting.stubs(:post_edit_time_limit).returns(5)
end
it 'returns false to the author of the post' do
Guardian.new(old_post.user).can_edit?(old_post).should eq(false)
end
it 'returns true as a moderator' do
Guardian.new(moderator).can_edit?(old_post).should eq(true)
end
it 'returns true as an admin' do
Guardian.new(admin).can_edit?(old_post).should eq(true)
end
it 'returns false for another regular user trying to edit your post' do
Guardian.new(coding_horror).can_edit?(old_post).should eq(false)
end
end
end end
describe 'a Topic' do describe 'a Topic' do
@ -773,6 +796,34 @@ describe Guardian do
it 'returns true when an admin' do it 'returns true when an admin' do
Guardian.new(admin).can_delete?(post).should be_true Guardian.new(admin).can_delete?(post).should be_true
end end
context 'post is older than post_edit_time_limit' do
let(:old_post) { build(:post, topic: topic, user: topic.user, post_number: 2, created_at: 6.minutes.ago) }
before do
SiteSetting.stubs(:post_edit_time_limit).returns(5)
end
it 'returns false to the author of the post' do
Guardian.new(old_post.user).can_delete?(old_post).should eq(false)
end
it 'returns true as a moderator' do
Guardian.new(moderator).can_delete?(old_post).should eq(true)
end
it 'returns true as an admin' do
Guardian.new(admin).can_delete?(old_post).should eq(true)
end
it "returns false when it's the OP, even as a moderator" do
old_post.post_number = 1
Guardian.new(moderator).can_delete?(old_post).should eq(false)
end
it 'returns false for another regular user trying to delete your post' do
Guardian.new(coding_horror).can_delete?(old_post).should eq(false)
end
end
end end
context 'a Category' do context 'a Category' do

View File

@ -214,7 +214,7 @@ describe PostsController do
end end
it "raises an error when the user doesn't have permission to see the post" do it "raises an error when the user doesn't have permission to see the post" do
Guardian.any_instance.expects(:can_edit?).with(post).returns(false) Guardian.any_instance.expects(:can_edit?).with(post).at_least_once.returns(false)
xhr :put, :update, update_params xhr :put, :update, update_params
response.should be_forbidden response.should be_forbidden
end end