Skip to content

Commit

Permalink
Add post_edit_time_limit site setting to limit the how long a post ca…
Browse files Browse the repository at this point in the history
…n be edited and deleted by the author. Default is 1 year.
  • Loading branch information
nlalonde committed Jan 9, 2014
1 parent e750ea0 commit 259295d
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 5 deletions.
12 changes: 11 additions & 1 deletion app/assets/javascripts/discourse/controllers/topic_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,17 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
}
]);
} else {
post.destroy(user);
post.destroy(user).then(null, function(e) {
console.log('Error case?');

This comment has been minimized.

Copy link
@ZogStriP

ZogStriP Jan 9, 2014

Member

You should install the JSHint plugin for SublimeText. It's awesomesauce!

This comment has been minimized.

Copy link
@nlalonde

nlalonde Jan 9, 2014

Author Member

Gah! Yeah I think I need it.

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'));
}
});
}
},

Expand Down
23 changes: 23 additions & 0 deletions app/assets/javascripts/discourse/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Discourse.Post = Discourse.Model.extend({
// We're updating a post
return Discourse.ajax("/posts/" + (this.get('id')), {
type: 'PUT',
dataType: 'json',
data: {
post: { raw: this.get('raw'), edit_reason: this.get('editReason') },
image_sizes: this.get('imageSizes')
Expand Down Expand Up @@ -236,6 +237,8 @@ Discourse.Post = Discourse.Model.extend({
@param {Discourse.User} deletedBy The user deleting the post
**/
setDeletedState: function(deletedBy) {
this.set('oldCooked', this.get('cooked'));

// Moderators can delete posts. Regular users can only trigger a deleted at message.
if (deletedBy.get('staff')) {
this.setProperties({
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,18 @@ class RenderEmpty < Exception; end
end

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

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

def rescue_discourse_actions(message, error)
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
else
render text: build_not_found_page(error, 'no_js')
Expand Down
12 changes: 12 additions & 0 deletions app/controllers/posts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ def update
post = post.with_deleted if guardian.is_staff?
post = post.first
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)

# to stay consistent with the create api,
Expand Down Expand Up @@ -127,6 +133,12 @@ def reply_history

def destroy
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)

destroyer = PostDestroyer.new(current_user, post)
Expand Down
8 changes: 8 additions & 0 deletions app/models/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ def revert_to(number)
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

def parse_quote_into_arguments(quote)
Expand Down
2 changes: 2 additions & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ en:
rss_description:
latest: "Latest topics"
hot: "Hot topics"
too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted."

groups:
errors:
Expand Down Expand Up @@ -544,6 +545,7 @@ en:
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)"
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."
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"
Expand Down
1 change: 1 addition & 0 deletions config/site_settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ posting:
default: 15
enable_private_messages: true
ninja_edit_window: 300
post_edit_time_limit: 525600
edit_history_visible_to_public:
client: true
default: true
Expand Down
5 changes: 4 additions & 1 deletion lib/guardian.rb
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def can_edit_category?(category)
end

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

def can_edit_user?(user)
Expand Down Expand Up @@ -304,6 +304,9 @@ def can_delete_post?(post)
# Can't delete the first post
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
return !post.user_deleted? if is_my_own?(post)

Expand Down
51 changes: 51 additions & 0 deletions spec/components/guardian_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,29 @@
it 'returns true as an admin' do
Guardian.new(admin).can_edit?(post).should be_true
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

describe 'a Topic' do
Expand Down Expand Up @@ -773,6 +796,34 @@
it 'returns true when an admin' do
Guardian.new(admin).can_delete?(post).should be_true
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

context 'a Category' do
Expand Down
2 changes: 1 addition & 1 deletion spec/controllers/posts_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
end

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
response.should be_forbidden
end
Expand Down

0 comments on commit 259295d

Please sign in to comment.