diff --git a/app/assets/javascripts/discourse/components/utilities.coffee b/app/assets/javascripts/discourse/components/utilities.coffee
index 69c445f9955..82f4c230475 100644
--- a/app/assets/javascripts/discourse/components/utilities.coffee
+++ b/app/assets/javascripts/discourse/components/utilities.coffee
@@ -155,7 +155,9 @@ Discourse.Utilities =
# Takes raw input and cooks it to display nicely (mostly markdown)
- cook: (raw, opts) ->
+ cook: (raw, opts=null) ->
+
+ opts ||= {}
# Make sure we've got a string
return "" unless raw
diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee b/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee
index 6449ba3730b..cd8101f2744 100644
--- a/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee
+++ b/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee
@@ -299,14 +299,15 @@ Discourse.TopicController = Ember.ObjectController.extend Discourse.Presence,
@get('controllers.modal')?.show(view)
false
+ recoverPost: (post) ->
+ post.set('deleted_at', null)
+ post.recover()
+
deletePost: (post) ->
-
- deleted = !!post.get('deleted_at')
-
- if deleted
- post.set('deleted_at', null)
+ if post.get('user_id') is Discourse.get('currentUser.id')
+ post.set('cooked', Discourse.Utilities.cook(Em.String.i18n("post.deleted_by_author")))
+ post.set('can_delete', false)
else
post.set('deleted_at', new Date())
- post.delete =>
- # nada
+ post.delete()
diff --git a/app/assets/javascripts/discourse/models/post.js.coffee.erb b/app/assets/javascripts/discourse/models/post.js.coffee.erb
index 64ab9743dc5..79177716b5d 100644
--- a/app/assets/javascripts/discourse/models/post.js.coffee.erb
+++ b/app/assets/javascripts/discourse/models/post.js.coffee.erb
@@ -153,8 +153,11 @@ window.Discourse.Post = Ember.Object.extend Discourse.Presence,
error: (result) -> error?(result)
+ recover: ->
+ $.ajax "/posts/#{@get('id')}/recover", type: 'PUT', cache: false
+
delete: (complete) ->
- $.ajax "/posts/#{@get('id')}", type: 'DELETE', success: (result) -> complete()
+ $.ajax "/posts/#{@get('id')}", type: 'DELETE', success: (result) -> complete?()
# Update the properties of this post from an obj, ignoring cooked as we should already
# have that rendered.
diff --git a/app/assets/javascripts/discourse/views/post_menu_view.js.coffee b/app/assets/javascripts/discourse/views/post_menu_view.js.coffee
index 65297ee70f8..9d67b30fb6c 100644
--- a/app/assets/javascripts/discourse/views/post_menu_view.js.coffee
+++ b/app/assets/javascripts/discourse/views/post_menu_view.js.coffee
@@ -26,7 +26,7 @@ window.Discourse.PostMenuView = Ember.View.extend Discourse.Presence,
# Trigger re rendering
needsToRender: (->
@rerender()
- ).observes('post.deleted_at', 'post.flagsAvailable.@each', 'post.url', 'post.bookmarked', 'post.reply_count', 'post.replyBelowUrl')
+ ).observes('post.deleted_at', 'post.flagsAvailable.@each', 'post.url', 'post.bookmarked', 'post.reply_count', 'post.replyBelowUrl', 'post.can_delete')
# Replies Button
renderReplies: (post, buffer) ->
@@ -49,11 +49,14 @@ window.Discourse.PostMenuView = Ember.View.extend Discourse.Presence,
# Delete button
renderDelete: (post, buffer) ->
- return unless post.get('can_delete')
- title = if post.get('deleted_at') then Em.String.i18n("post.controls.undelete") else Em.String.i18n("post.controls.delete")
- buffer.push("")
+ if post.get('deleted_at')
+ if post.get('can_recover')
+ buffer.push("")
+ else if post.get('can_delete')
+ buffer.push("")
+ clickRecover: -> @get('controller').recoverPost(@get('post'))
clickDelete: -> @get('controller').deletePost(@get('post'))
# Like button
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index d503b66b0a6..ce4d2e9ba43 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -74,16 +74,16 @@ class PostsController < ApplicationController
end
def destroy
- Post.transaction do
- post = Post.with_deleted.where(id: params[:id]).first
- guardian.ensure_can_delete!(post)
- if post.deleted_at.nil?
- post.destroy
- else
- post.recover
- end
- Topic.reset_highest(post.topic_id)
- end
+ post = Post.where(id: params[:id]).first
+ guardian.ensure_can_delete!(post)
+ post.delete_by(current_user)
+ render nothing: true
+ end
+
+ def recover
+ post = Post.with_deleted.where(id: params[:post_id]).first
+ guardian.ensure_can_recover_post!(post)
+ post.recover
render nothing: true
end
diff --git a/app/models/post.rb b/app/models/post.rb
index 6617c57a712..9482b932146 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -159,7 +159,23 @@ class Post < ActiveRecord::Base
else
@raw_mentions = []
end
+ end
+ # The rules for deletion change depending on who is doing it.
+ def delete_by(deleted_by)
+ if deleted_by.has_trust_level?(:moderator)
+ # As a moderator, delete the post.
+ Post.transaction do
+ self.destroy
+ Topic.reset_highest(self.topic_id)
+ end
+ elsif deleted_by.id == self.user_id
+ # As the poster, make a revision that says deleted.
+ Post.transaction do
+ revise(deleted_by, I18n.t('js.post.deleted_by_author'), force_new_version: true)
+ update_column(:user_deleted, true)
+ end
+ end
end
def archetype
@@ -311,6 +327,8 @@ class Post < ActiveRecord::Base
# We always create a new version if it's been greater than the ninja edit window
new_version = true if (revised_at - last_version_at) > SiteSetting.ninja_edit_window.to_i
+ new_version = true if opts[:force_new_version]
+
# Create the new version (or don't)
if new_version
diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb
index f51913206ed..8e5f7437995 100644
--- a/app/serializers/post_serializer.rb
+++ b/app/serializers/post_serializer.rb
@@ -28,6 +28,7 @@ class PostSerializer < ApplicationSerializer
:version,
:can_edit,
:can_delete,
+ :can_recover,
:link_counts,
:cooked,
:read,
@@ -66,6 +67,10 @@ class PostSerializer < ApplicationSerializer
scope.can_delete?(object)
end
+ def can_recover
+ scope.can_recover_post?(object)
+ end
+
def link_counts
return @single_post_link_counts if @single_post_link_counts.present?
diff --git a/config/locales/en.yml b/config/locales/en.yml
index db014fc301a..88b9d9626a3 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -831,6 +831,7 @@ en:
reply_as_new_topic: "Reply as new Topic"
continue_discussion: "Continuing the discussion from {{postLink}}:"
follow_quote: "go to the quoted post"
+ deleted_by_author: "Post deleted by author."
has_replies_below:
one: "Reply Below"
diff --git a/config/routes.rb b/config/routes.rb
index 9bcd44afd2a..c81a86d1776 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -103,6 +103,7 @@ Discourse::Application.routes.draw do
get 'versions'
put 'bookmark'
get 'replies'
+ put 'recover'
collection do
delete 'destroy_many'
end
diff --git a/db/migrate/20130207200019_add_user_deleted_to_posts.rb b/db/migrate/20130207200019_add_user_deleted_to_posts.rb
new file mode 100644
index 00000000000..04e45a1a0b2
--- /dev/null
+++ b/db/migrate/20130207200019_add_user_deleted_to_posts.rb
@@ -0,0 +1,5 @@
+class AddUserDeletedToPosts < ActiveRecord::Migration
+ def change
+ add_column :posts, :user_deleted, :boolean, null: false, default: false
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index dc1b587779a..b7456f03e7e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1798,7 +1798,8 @@ CREATE TABLE posts (
spam_count integer DEFAULT 0 NOT NULL,
illegal_count integer DEFAULT 0 NOT NULL,
inappropriate_count integer DEFAULT 0 NOT NULL,
- last_version_at timestamp without time zone NOT NULL
+ last_version_at timestamp without time zone NOT NULL,
+ user_deleted boolean DEFAULT false NOT NULL
);
@@ -4569,4 +4570,6 @@ INSERT INTO schema_migrations (version) VALUES ('20130203204338');
INSERT INTO schema_migrations (version) VALUES ('20130204000159');
-INSERT INTO schema_migrations (version) VALUES ('20130205021905');
\ No newline at end of file
+INSERT INTO schema_migrations (version) VALUES ('20130205021905');
+
+INSERT INTO schema_migrations (version) VALUES ('20130207200019');
\ No newline at end of file
diff --git a/lib/guardian.rb b/lib/guardian.rb
index 48b4d412871..7654a62fa19 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -232,6 +232,15 @@ class Guardian
# Can't delete the first post
return false if post.post_number == 1
+ # You can delete your own posts
+ return !post.user_deleted? if post.user == @user
+
+ @user.has_trust_level?(:moderator)
+ end
+
+ # Recovery Method
+ def can_recover_post?(post)
+ return false if @user.blank?
@user.has_trust_level?(:moderator)
end
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 3a1d4773208..355c2324ce9 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -361,6 +361,26 @@ describe Guardian do
end
end
+ describe "can_recover_post?" do
+
+ it "returns false for a nil user" do
+ Guardian.new(nil).can_recover_post?(post).should be_false
+ end
+
+ it "returns false for a nil object" do
+ Guardian.new(user).can_recover_post?(nil).should be_false
+ end
+
+ it "returns false for a regular user" do
+ Guardian.new(user).can_recover_post?(post).should be_false
+ end
+
+ it "returns true for a moderator" do
+ Guardian.new(moderator).can_recover_post?(post).should be_true
+ end
+
+ end
+
describe 'can_edit?' do
it 'returns false with a nil object' do
@@ -576,10 +596,20 @@ describe Guardian do
Guardian.new.can_delete?(post).should be_false
end
- it 'returns false when not a moderator' do
+ it "returns false when trying to delete your own post that has already been deleted" do
+ post.delete_by(user)
+ post.reload
Guardian.new(user).can_delete?(post).should be_false
end
+ it 'returns true when trying to delete your own post' do
+ Guardian.new(user).can_delete?(post).should be_true
+ end
+
+ it "returns false when trying to delete another user's own post" do
+ Guardian.new(Fabricate(:user)).can_delete?(post).should be_false
+ end
+
it "returns false when it's the OP, even as a moderator" do
post.update_attribute :post_number, 1
Guardian.new(moderator).can_delete?(post).should be_false
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index a96c16cc3f9..302da485e01 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -51,7 +51,8 @@ describe PostsController do
describe 'when logged in' do
- let(:post) { Fabricate(:post, user: log_in(:moderator), post_number: 2) }
+ let(:user) { log_in(:moderator) }
+ let(:post) { Fabricate(:post, user: user, post_number: 2) }
it "raises an error when the user doesn't have permission to see the post" do
Guardian.any_instance.expects(:can_delete?).with(post).returns(false)
@@ -59,19 +60,39 @@ describe PostsController do
response.should be_forbidden
end
- it "deletes the post" do
- Post.any_instance.expects(:destroy)
- xhr :delete, :destroy, id: post.id
- end
-
- it "updates the highest read data for the forum" do
- Topic.expects(:reset_highest).with(post.topic_id)
+ it "calls delete_by" do
+ Post.any_instance.expects(:delete_by).with(user)
xhr :delete, :destroy, id: post.id
end
end
end
+ describe 'recover a post' do
+ it 'raises an exception when not logged in' do
+ lambda { xhr :put, :recover, post_id: 123 }.should raise_error(Discourse::NotLoggedIn)
+ end
+
+ describe 'when logged in' do
+
+ let(:user) { log_in(:moderator) }
+ let(:post) { Fabricate(:post, user: user, post_number: 2) }
+
+ it "raises an error when the user doesn't have permission to see the post" do
+ Guardian.any_instance.expects(:can_recover_post?).with(post).returns(false)
+ xhr :put, :recover, post_id: post.id
+ response.should be_forbidden
+ end
+
+ it "calls recover" do
+ Post.any_instance.expects(:recover)
+ xhr :put, :recover, post_id: post.id
+ end
+
+ end
+ end
+
+
describe 'destroy_many' do
it 'raises an exception when not logged in' do
lambda { xhr :delete, :destroy_many, post_ids: [123, 345] }.should raise_error(Discourse::NotLoggedIn)
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index ec698f02e0a..79fdd7921aa 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -505,6 +505,48 @@ describe Post do
end
+ describe 'delete_by' do
+
+ let(:moderator) { Fabricate(:moderator) }
+ let(:post) { Fabricate(:post) }
+
+ context "as the creator of the post" do
+
+ before do
+ post.delete_by(post.user)
+ post.reload
+ end
+
+ it "doesn't delete the post" do
+ post.deleted_at.should be_blank
+ end
+
+ it "updates the text of the post" do
+ post.raw.should == I18n.t('js.post.deleted_by_author')
+ end
+
+
+ it "creates a new version" do
+ post.version.should == 2
+ end
+
+ end
+
+ context "as a moderator" do
+
+ before do
+ post.delete_by(post.user)
+ post.reload
+ end
+
+ it "deletes the post" do
+ post.deleted_at.should be_blank
+ end
+
+ end
+
+ end
+
describe 'after delete' do
let!(:coding_horror) { Fabricate(:coding_horror) }
@@ -550,6 +592,10 @@ describe Post do
let(:post) { Fabricate(:post, post_args) }
+ it "defaults to not user_deleted" do
+ post.user_deleted?.should be_false
+ end
+
it 'has a post nubmer' do
post.post_number.should be_present
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index fa9098f8d59..636bd1ae58a 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -88,6 +88,7 @@ end
Spork.each_run do
# This code will be run each time you run your specs.
$redis.client.reconnect
+ MessageBus.reliable_pub_sub.pub_redis.client.reconnect
end
# --- Instructions ---