mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 07:31:12 +08:00
Give regular users a delete button. If they click it, their post will be revised to
say it was deleted.
This commit is contained in:
@ -155,7 +155,9 @@ Discourse.Utilities =
|
|||||||
|
|
||||||
|
|
||||||
# Takes raw input and cooks it to display nicely (mostly markdown)
|
# 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
|
# Make sure we've got a string
|
||||||
return "" unless raw
|
return "" unless raw
|
||||||
|
@ -299,14 +299,15 @@ Discourse.TopicController = Ember.ObjectController.extend Discourse.Presence,
|
|||||||
@get('controllers.modal')?.show(view)
|
@get('controllers.modal')?.show(view)
|
||||||
false
|
false
|
||||||
|
|
||||||
|
recoverPost: (post) ->
|
||||||
|
post.set('deleted_at', null)
|
||||||
|
post.recover()
|
||||||
|
|
||||||
deletePost: (post) ->
|
deletePost: (post) ->
|
||||||
|
if post.get('user_id') is Discourse.get('currentUser.id')
|
||||||
deleted = !!post.get('deleted_at')
|
post.set('cooked', Discourse.Utilities.cook(Em.String.i18n("post.deleted_by_author")))
|
||||||
|
post.set('can_delete', false)
|
||||||
if deleted
|
|
||||||
post.set('deleted_at', null)
|
|
||||||
else
|
else
|
||||||
post.set('deleted_at', new Date())
|
post.set('deleted_at', new Date())
|
||||||
|
|
||||||
post.delete =>
|
post.delete()
|
||||||
# nada
|
|
||||||
|
@ -153,8 +153,11 @@ window.Discourse.Post = Ember.Object.extend Discourse.Presence,
|
|||||||
error: (result) -> error?(result)
|
error: (result) -> error?(result)
|
||||||
|
|
||||||
|
|
||||||
|
recover: ->
|
||||||
|
$.ajax "/posts/#{@get('id')}/recover", type: 'PUT', cache: false
|
||||||
|
|
||||||
delete: (complete) ->
|
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
|
# Update the properties of this post from an obj, ignoring cooked as we should already
|
||||||
# have that rendered.
|
# have that rendered.
|
||||||
|
@ -26,7 +26,7 @@ window.Discourse.PostMenuView = Ember.View.extend Discourse.Presence,
|
|||||||
# Trigger re rendering
|
# Trigger re rendering
|
||||||
needsToRender: (->
|
needsToRender: (->
|
||||||
@rerender()
|
@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
|
# Replies Button
|
||||||
renderReplies: (post, buffer) ->
|
renderReplies: (post, buffer) ->
|
||||||
@ -49,11 +49,14 @@ window.Discourse.PostMenuView = Ember.View.extend Discourse.Presence,
|
|||||||
|
|
||||||
# Delete button
|
# Delete button
|
||||||
renderDelete: (post, buffer) ->
|
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")
|
if post.get('deleted_at')
|
||||||
buffer.push("<button title=\"#{title}\" data-action=\"delete\"><i class=\"icon-trash\"></i></button>")
|
if post.get('can_recover')
|
||||||
|
buffer.push("<button title=\"#{Em.String.i18n("post.controls.undelete")}\" data-action=\"recover\"><i class=\"icon-undo\"></i></button>")
|
||||||
|
else if post.get('can_delete')
|
||||||
|
buffer.push("<button title=\"#{Em.String.i18n("post.controls.delete")}\" data-action=\"delete\"><i class=\"icon-trash\"></i></button>")
|
||||||
|
|
||||||
|
clickRecover: -> @get('controller').recoverPost(@get('post'))
|
||||||
clickDelete: -> @get('controller').deletePost(@get('post'))
|
clickDelete: -> @get('controller').deletePost(@get('post'))
|
||||||
|
|
||||||
# Like button
|
# Like button
|
||||||
|
@ -74,16 +74,16 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
Post.transaction do
|
post = Post.where(id: params[:id]).first
|
||||||
post = Post.with_deleted.where(id: params[:id]).first
|
guardian.ensure_can_delete!(post)
|
||||||
guardian.ensure_can_delete!(post)
|
post.delete_by(current_user)
|
||||||
if post.deleted_at.nil?
|
render nothing: true
|
||||||
post.destroy
|
end
|
||||||
else
|
|
||||||
post.recover
|
def recover
|
||||||
end
|
post = Post.with_deleted.where(id: params[:post_id]).first
|
||||||
Topic.reset_highest(post.topic_id)
|
guardian.ensure_can_recover_post!(post)
|
||||||
end
|
post.recover
|
||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -159,7 +159,23 @@ class Post < ActiveRecord::Base
|
|||||||
else
|
else
|
||||||
@raw_mentions = []
|
@raw_mentions = []
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def archetype
|
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
|
# 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 (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)
|
# Create the new version (or don't)
|
||||||
if new_version
|
if new_version
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ class PostSerializer < ApplicationSerializer
|
|||||||
:version,
|
:version,
|
||||||
:can_edit,
|
:can_edit,
|
||||||
:can_delete,
|
:can_delete,
|
||||||
|
:can_recover,
|
||||||
:link_counts,
|
:link_counts,
|
||||||
:cooked,
|
:cooked,
|
||||||
:read,
|
:read,
|
||||||
@ -66,6 +67,10 @@ class PostSerializer < ApplicationSerializer
|
|||||||
scope.can_delete?(object)
|
scope.can_delete?(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_recover
|
||||||
|
scope.can_recover_post?(object)
|
||||||
|
end
|
||||||
|
|
||||||
def link_counts
|
def link_counts
|
||||||
|
|
||||||
return @single_post_link_counts if @single_post_link_counts.present?
|
return @single_post_link_counts if @single_post_link_counts.present?
|
||||||
|
@ -831,6 +831,7 @@ en:
|
|||||||
reply_as_new_topic: "Reply as new Topic"
|
reply_as_new_topic: "Reply as new Topic"
|
||||||
continue_discussion: "Continuing the discussion from {{postLink}}:"
|
continue_discussion: "Continuing the discussion from {{postLink}}:"
|
||||||
follow_quote: "go to the quoted post"
|
follow_quote: "go to the quoted post"
|
||||||
|
deleted_by_author: "Post deleted by author."
|
||||||
|
|
||||||
has_replies_below:
|
has_replies_below:
|
||||||
one: "Reply Below"
|
one: "Reply Below"
|
||||||
|
@ -103,6 +103,7 @@ Discourse::Application.routes.draw do
|
|||||||
get 'versions'
|
get 'versions'
|
||||||
put 'bookmark'
|
put 'bookmark'
|
||||||
get 'replies'
|
get 'replies'
|
||||||
|
put 'recover'
|
||||||
collection do
|
collection do
|
||||||
delete 'destroy_many'
|
delete 'destroy_many'
|
||||||
end
|
end
|
||||||
|
5
db/migrate/20130207200019_add_user_deleted_to_posts.rb
Normal file
5
db/migrate/20130207200019_add_user_deleted_to_posts.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class AddUserDeletedToPosts < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :posts, :user_deleted, :boolean, null: false, default: false
|
||||||
|
end
|
||||||
|
end
|
@ -1798,7 +1798,8 @@ CREATE TABLE posts (
|
|||||||
spam_count integer DEFAULT 0 NOT NULL,
|
spam_count integer DEFAULT 0 NOT NULL,
|
||||||
illegal_count integer DEFAULT 0 NOT NULL,
|
illegal_count integer DEFAULT 0 NOT NULL,
|
||||||
inappropriate_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 ('20130204000159');
|
||||||
|
|
||||||
INSERT INTO schema_migrations (version) VALUES ('20130205021905');
|
INSERT INTO schema_migrations (version) VALUES ('20130205021905');
|
||||||
|
|
||||||
|
INSERT INTO schema_migrations (version) VALUES ('20130207200019');
|
@ -232,6 +232,15 @@ 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
|
||||||
|
|
||||||
|
# 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)
|
@user.has_trust_level?(:moderator)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -361,6 +361,26 @@ describe Guardian do
|
|||||||
end
|
end
|
||||||
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
|
describe 'can_edit?' do
|
||||||
|
|
||||||
it 'returns false with a nil object' do
|
it 'returns false with a nil object' do
|
||||||
@ -576,10 +596,20 @@ describe Guardian do
|
|||||||
Guardian.new.can_delete?(post).should be_false
|
Guardian.new.can_delete?(post).should be_false
|
||||||
end
|
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
|
Guardian.new(user).can_delete?(post).should be_false
|
||||||
end
|
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
|
it "returns false when it's the OP, even as a moderator" do
|
||||||
post.update_attribute :post_number, 1
|
post.update_attribute :post_number, 1
|
||||||
Guardian.new(moderator).can_delete?(post).should be_false
|
Guardian.new(moderator).can_delete?(post).should be_false
|
||||||
|
@ -51,7 +51,8 @@ describe PostsController do
|
|||||||
|
|
||||||
describe 'when logged in' 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
|
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)
|
Guardian.any_instance.expects(:can_delete?).with(post).returns(false)
|
||||||
@ -59,19 +60,39 @@ describe PostsController do
|
|||||||
response.should be_forbidden
|
response.should be_forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
it "deletes the post" do
|
it "calls delete_by" do
|
||||||
Post.any_instance.expects(:destroy)
|
Post.any_instance.expects(:delete_by).with(user)
|
||||||
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)
|
|
||||||
xhr :delete, :destroy, id: post.id
|
xhr :delete, :destroy, id: post.id
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
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
|
describe 'destroy_many' do
|
||||||
it 'raises an exception when not logged in' do
|
it 'raises an exception when not logged in' do
|
||||||
lambda { xhr :delete, :destroy_many, post_ids: [123, 345] }.should raise_error(Discourse::NotLoggedIn)
|
lambda { xhr :delete, :destroy_many, post_ids: [123, 345] }.should raise_error(Discourse::NotLoggedIn)
|
||||||
|
@ -505,6 +505,48 @@ describe Post do
|
|||||||
|
|
||||||
end
|
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
|
describe 'after delete' do
|
||||||
|
|
||||||
let!(:coding_horror) { Fabricate(:coding_horror) }
|
let!(:coding_horror) { Fabricate(:coding_horror) }
|
||||||
@ -550,6 +592,10 @@ describe Post do
|
|||||||
|
|
||||||
let(:post) { Fabricate(:post, post_args) }
|
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
|
it 'has a post nubmer' do
|
||||||
post.post_number.should be_present
|
post.post_number.should be_present
|
||||||
end
|
end
|
||||||
|
@ -88,6 +88,7 @@ end
|
|||||||
Spork.each_run do
|
Spork.each_run do
|
||||||
# This code will be run each time you run your specs.
|
# This code will be run each time you run your specs.
|
||||||
$redis.client.reconnect
|
$redis.client.reconnect
|
||||||
|
MessageBus.reliable_pub_sub.pub_redis.client.reconnect
|
||||||
end
|
end
|
||||||
|
|
||||||
# --- Instructions ---
|
# --- Instructions ---
|
||||||
|
Reference in New Issue
Block a user