mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-12-17 19:12:37 -05:00
FEATURE: log topic/post deletions from staff members
This commit is contained in:
parent
be93f224a6
commit
98b6b9821a
14 changed files with 155 additions and 33 deletions
|
@ -17,6 +17,8 @@ Discourse.StaffActionLog = Discourse.Model.extend({
|
||||||
var formatted = "";
|
var formatted = "";
|
||||||
formatted += this.format('email', 'email');
|
formatted += this.format('email', 'email');
|
||||||
formatted += this.format('admin.logs.ip_address', 'ip_address');
|
formatted += this.format('admin.logs.ip_address', 'ip_address');
|
||||||
|
formatted += this.format('admin.logs.topic_id', 'topic_id');
|
||||||
|
formatted += this.format('admin.logs.post_id', 'post_id');
|
||||||
if (!this.get('useCustomModalForDetails')) {
|
if (!this.get('useCustomModalForDetails')) {
|
||||||
formatted += this.format('admin.logs.staff_actions.new_value', 'new_value');
|
formatted += this.format('admin.logs.staff_actions.new_value', 'new_value');
|
||||||
formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value');
|
formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value');
|
||||||
|
@ -25,7 +27,7 @@ Discourse.StaffActionLog = Discourse.Model.extend({
|
||||||
if (this.get('details')) formatted += Handlebars.Utils.escapeExpression(this.get('details')) + '<br/>';
|
if (this.get('details')) formatted += Handlebars.Utils.escapeExpression(this.get('details')) + '<br/>';
|
||||||
}
|
}
|
||||||
return formatted;
|
return formatted;
|
||||||
}.property('ip_address', 'email'),
|
}.property('ip_address', 'email', 'topic_id', 'post_id'),
|
||||||
|
|
||||||
format: function(label, propertyName) {
|
format: function(label, propertyName) {
|
||||||
if (this.get(propertyName)) {
|
if (this.get(propertyName)) {
|
||||||
|
|
|
@ -280,7 +280,10 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
**/
|
**/
|
||||||
destroy: function(deletedBy) {
|
destroy: function(deletedBy) {
|
||||||
this.setDeletedState(deletedBy);
|
this.setDeletedState(deletedBy);
|
||||||
return Discourse.ajax("/posts/" + (this.get('id')), { type: 'DELETE' });
|
return Discourse.ajax("/posts/" + this.get('id'), {
|
||||||
|
data: { context: window.location.pathname },
|
||||||
|
type: 'DELETE'
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -215,7 +215,10 @@ Discourse.Topic = Discourse.Model.extend({
|
||||||
'details.can_delete': false,
|
'details.can_delete': false,
|
||||||
'details.can_recover': true
|
'details.can_recover': true
|
||||||
});
|
});
|
||||||
return Discourse.ajax("/t/" + this.get('id'), { type: 'DELETE' });
|
return Discourse.ajax("/t/" + this.get('id'), {
|
||||||
|
data: { context: window.location.pathname },
|
||||||
|
type: 'DELETE'
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Recover this topic if deleted
|
// Recover this topic if deleted
|
||||||
|
|
|
@ -167,7 +167,7 @@ class PostsController < ApplicationController
|
||||||
|
|
||||||
guardian.ensure_can_delete!(post)
|
guardian.ensure_can_delete!(post)
|
||||||
|
|
||||||
destroyer = PostDestroyer.new(current_user, post)
|
destroyer = PostDestroyer.new(current_user, post, { context: params[:context] })
|
||||||
destroyer.destroy
|
destroyer.destroy
|
||||||
|
|
||||||
render nothing: true
|
render nothing: true
|
||||||
|
|
|
@ -211,7 +211,7 @@ class TopicsController < ApplicationController
|
||||||
guardian.ensure_can_delete!(topic)
|
guardian.ensure_can_delete!(topic)
|
||||||
|
|
||||||
first_post = topic.ordered_posts.first
|
first_post = topic.ordered_posts.first
|
||||||
PostDestroyer.new(current_user, first_post).destroy
|
PostDestroyer.new(current_user, first_post, { context: params[:context] }).destroy
|
||||||
|
|
||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,9 @@ class UserHistory < ActiveRecord::Base
|
||||||
belongs_to :acting_user, class_name: 'User'
|
belongs_to :acting_user, class_name: 'User'
|
||||||
belongs_to :target_user, class_name: 'User'
|
belongs_to :target_user, class_name: 'User'
|
||||||
|
|
||||||
|
belongs_to :post
|
||||||
|
belongs_to :topic
|
||||||
|
|
||||||
validates_presence_of :action
|
validates_presence_of :action
|
||||||
|
|
||||||
scope :only_staff_actions, ->{ where("action IN (?)", UserHistory.staff_action_ids) }
|
scope :only_staff_actions, ->{ where("action IN (?)", UserHistory.staff_action_ids) }
|
||||||
|
@ -12,7 +15,7 @@ class UserHistory < ActiveRecord::Base
|
||||||
before_save :set_admin_only
|
before_save :set_admin_only
|
||||||
|
|
||||||
def self.actions
|
def self.actions
|
||||||
@actions ||= Enum.new( :delete_user,
|
@actions ||= Enum.new(:delete_user,
|
||||||
:change_trust_level,
|
:change_trust_level,
|
||||||
:change_site_setting,
|
:change_site_setting,
|
||||||
:change_site_customization,
|
:change_site_customization,
|
||||||
|
@ -27,7 +30,9 @@ class UserHistory < ActiveRecord::Base
|
||||||
:grant_badge,
|
:grant_badge,
|
||||||
:revoke_badge,
|
:revoke_badge,
|
||||||
:auto_trust_level_change,
|
:auto_trust_level_change,
|
||||||
:check_email)
|
:check_email,
|
||||||
|
:delete_post,
|
||||||
|
:delete_topic)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Staff actions is a subset of all actions, used to audit actions taken by staff users.
|
# Staff actions is a subset of all actions, used to audit actions taken by staff users.
|
||||||
|
@ -41,7 +46,9 @@ class UserHistory < ActiveRecord::Base
|
||||||
:unsuspend_user,
|
:unsuspend_user,
|
||||||
:grant_badge,
|
:grant_badge,
|
||||||
:revoke_badge,
|
:revoke_badge,
|
||||||
:check_email]
|
:check_email,
|
||||||
|
:delete_post,
|
||||||
|
:delete_topic]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.staff_action_ids
|
def self.staff_action_ids
|
||||||
|
|
|
@ -7,7 +7,9 @@ class UserHistorySerializer < ApplicationSerializer
|
||||||
:created_at,
|
:created_at,
|
||||||
:subject,
|
:subject,
|
||||||
:previous_value,
|
:previous_value,
|
||||||
:new_value
|
:new_value,
|
||||||
|
:topic_id,
|
||||||
|
:post_id
|
||||||
|
|
||||||
has_one :acting_user, serializer: BasicUserSerializer, embed: :objects
|
has_one :acting_user, serializer: BasicUserSerializer, embed: :objects
|
||||||
has_one :target_user, serializer: BasicUserSerializer, embed: :objects
|
has_one :target_user, serializer: BasicUserSerializer, embed: :objects
|
||||||
|
|
|
@ -2,32 +2,74 @@
|
||||||
class StaffActionLogger
|
class StaffActionLogger
|
||||||
def initialize(admin)
|
def initialize(admin)
|
||||||
@admin = admin
|
@admin = admin
|
||||||
raise Discourse::InvalidParameters.new('admin is nil') unless @admin and @admin.is_a?(User)
|
raise Discourse::InvalidParameters.new('admin is nil') unless @admin && @admin.is_a?(User)
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_user_deletion(deleted_user, opts={})
|
def log_user_deletion(deleted_user, opts={})
|
||||||
raise Discourse::InvalidParameters.new('user is nil') unless deleted_user and deleted_user.is_a?(User)
|
raise Discourse::InvalidParameters.new('user is nil') unless deleted_user && deleted_user.is_a?(User)
|
||||||
UserHistory.create( params(opts).merge({
|
UserHistory.create( params(opts).merge({
|
||||||
action: UserHistory.actions[:delete_user],
|
action: UserHistory.actions[:delete_user],
|
||||||
email: deleted_user.email,
|
email: deleted_user.email,
|
||||||
ip_address: deleted_user.ip_address.to_s,
|
ip_address: deleted_user.ip_address.to_s,
|
||||||
details: [:id, :username, :name, :created_at, :trust_level, :last_seen_at, :last_emailed_at].map { |x| "#{x}: #{deleted_user.send(x)}" }.join(', ')
|
details: [:id, :username, :name, :created_at, :trust_level, :last_seen_at, :last_emailed_at].map { |x| "#{x}: #{deleted_user.send(x)}" }.join("\n")
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_post_deletion(deleted_post, opts={})
|
||||||
|
raise Discourse::InvalidParameters.new("post is nil") unless deleted_post && deleted_post.is_a?(Post)
|
||||||
|
|
||||||
|
topic = deleted_post.topic || Topic.with_deleted.find(deleted_post.topic_id)
|
||||||
|
|
||||||
|
details = [
|
||||||
|
"id: #{deleted_post.id}",
|
||||||
|
"created_at: #{deleted_post.created_at}",
|
||||||
|
"user: #{deleted_post.user.username} (#{deleted_post.user.name})",
|
||||||
|
"topic: #{topic.title}",
|
||||||
|
"post_number: #{deleted_post.post_number}",
|
||||||
|
"raw: #{deleted_post.raw}"
|
||||||
|
]
|
||||||
|
|
||||||
|
UserHistory.create(params(opts).merge({
|
||||||
|
action: UserHistory.actions[:delete_post],
|
||||||
|
post_id: deleted_post.id,
|
||||||
|
details: details.join("\n")
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_topic_deletion(deleted_topic, opts={})
|
||||||
|
raise Discourse::InvalidParameters.new("topic is nil") unless deleted_topic && deleted_topic.is_a?(Topic)
|
||||||
|
|
||||||
|
details = [
|
||||||
|
"id: #{deleted_topic.id}",
|
||||||
|
"created_at: #{deleted_topic.created_at}",
|
||||||
|
"user: #{deleted_topic.user.username} (#{deleted_topic.user.name})",
|
||||||
|
"title: #{deleted_topic.title}"
|
||||||
|
]
|
||||||
|
|
||||||
|
if first_post = deleted_topic.ordered_posts.first
|
||||||
|
details << "raw: #{first_post.raw}"
|
||||||
|
end
|
||||||
|
|
||||||
|
UserHistory.create(params(opts).merge({
|
||||||
|
action: UserHistory.actions[:delete_topic],
|
||||||
|
topic_id: deleted_topic.id,
|
||||||
|
details: details.join("\n")
|
||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_trust_level_change(user, old_trust_level, new_trust_level, opts={})
|
def log_trust_level_change(user, old_trust_level, new_trust_level, opts={})
|
||||||
raise Discourse::InvalidParameters.new('user is nil') unless user and user.is_a?(User)
|
raise Discourse::InvalidParameters.new('user is nil') unless user && user.is_a?(User)
|
||||||
raise Discourse::InvalidParameters.new('old trust level is invalid') unless TrustLevel.valid? old_trust_level
|
raise Discourse::InvalidParameters.new('old trust level is invalid') unless TrustLevel.valid? old_trust_level
|
||||||
raise Discourse::InvalidParameters.new('new trust level is invalid') unless TrustLevel.valid? new_trust_level
|
raise Discourse::InvalidParameters.new('new trust level is invalid') unless TrustLevel.valid? new_trust_level
|
||||||
UserHistory.create!( params(opts).merge({
|
UserHistory.create!( params(opts).merge({
|
||||||
action: UserHistory.actions[:change_trust_level],
|
action: UserHistory.actions[:change_trust_level],
|
||||||
target_user_id: user.id,
|
target_user_id: user.id,
|
||||||
details: "old trust level: #{old_trust_level}, new trust level: #{new_trust_level}"
|
details: "old trust level: #{old_trust_level}\nnew trust level: #{new_trust_level}"
|
||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_site_setting_change(setting_name, previous_value, new_value, opts={})
|
def log_site_setting_change(setting_name, previous_value, new_value, opts={})
|
||||||
raise Discourse::InvalidParameters.new('setting_name is invalid') unless setting_name.present? and SiteSetting.respond_to?(setting_name)
|
raise Discourse::InvalidParameters.new('setting_name is invalid') unless setting_name.present? && SiteSetting.respond_to?(setting_name)
|
||||||
UserHistory.create( params(opts).merge({
|
UserHistory.create( params(opts).merge({
|
||||||
action: UserHistory.actions[:change_site_setting],
|
action: UserHistory.actions[:change_site_setting],
|
||||||
subject: setting_name,
|
subject: setting_name,
|
||||||
|
|
|
@ -1768,6 +1768,8 @@ en:
|
||||||
last_match_at: "Last Matched"
|
last_match_at: "Last Matched"
|
||||||
match_count: "Matches"
|
match_count: "Matches"
|
||||||
ip_address: "IP"
|
ip_address: "IP"
|
||||||
|
topic_id: "Topic ID"
|
||||||
|
post_id: "Post ID"
|
||||||
delete: 'Delete'
|
delete: 'Delete'
|
||||||
edit: 'Edit'
|
edit: 'Edit'
|
||||||
save: 'Save'
|
save: 'Save'
|
||||||
|
@ -1802,6 +1804,8 @@ en:
|
||||||
grant_badge: "grant badge"
|
grant_badge: "grant badge"
|
||||||
revoke_badge: "revoke badge"
|
revoke_badge: "revoke badge"
|
||||||
check_email: "check email"
|
check_email: "check email"
|
||||||
|
delete_topic: "delete topic"
|
||||||
|
delete_post: "delete post"
|
||||||
screened_emails:
|
screened_emails:
|
||||||
title: "Screened Emails"
|
title: "Screened Emails"
|
||||||
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddPostIdToUserHistories < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :user_histories, :post_id, :integer
|
||||||
|
end
|
||||||
|
end
|
|
@ -33,10 +33,11 @@ class PostDestroyer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(user, post)
|
def initialize(user, post, opts={})
|
||||||
@user = user
|
@user = user
|
||||||
@post = post
|
@post = post
|
||||||
@topic = post.topic if post
|
@topic = post.topic if post
|
||||||
|
@opts = opts
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -79,7 +80,12 @@ class PostDestroyer
|
||||||
@post.update_flagged_posts_count
|
@post.update_flagged_posts_count
|
||||||
remove_associated_replies
|
remove_associated_replies
|
||||||
remove_associated_notifications
|
remove_associated_notifications
|
||||||
@post.topic.trash!(@user) if @post.topic && @post.post_number == 1
|
if @post.topic && @post.post_number == 1
|
||||||
|
StaffActionLogger.new(@user).log_topic_deletion(@post.topic, @opts.slice(:context))
|
||||||
|
@post.topic.trash!(@user)
|
||||||
|
else
|
||||||
|
StaffActionLogger.new(@user).log_post_deletion(@post, @opts.slice(:context))
|
||||||
|
end
|
||||||
update_associated_category_latest_topic
|
update_associated_category_latest_topic
|
||||||
update_user_counts
|
update_user_counts
|
||||||
end
|
end
|
||||||
|
|
|
@ -170,6 +170,12 @@ describe PostDestroyer do
|
||||||
author.reload
|
author.reload
|
||||||
}.to change { author.post_count }.by(-1)
|
}.to change { author.post_count }.by(-1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "creates a new user history entry" do
|
||||||
|
expect {
|
||||||
|
PostDestroyer.new(moderator, post).destroy
|
||||||
|
}.to change { UserHistory.count}.by(1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "as an admin" do
|
context "as an admin" do
|
||||||
|
@ -258,6 +264,12 @@ describe PostDestroyer do
|
||||||
post.deleted_at.should be_present
|
post.deleted_at.should be_present
|
||||||
post.deleted_by.should == admin
|
post.deleted_by.should == admin
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "creates a new user history entry" do
|
||||||
|
expect {
|
||||||
|
PostDestroyer.new(admin, post).destroy
|
||||||
|
}.to change { UserHistory.count}.by(1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,7 @@ describe PostsController do
|
||||||
|
|
||||||
it "uses a PostDestroyer" do
|
it "uses a PostDestroyer" do
|
||||||
destroyer = mock
|
destroyer = mock
|
||||||
PostDestroyer.expects(:new).with(user, post).returns(destroyer)
|
PostDestroyer.expects(:new).returns(destroyer)
|
||||||
destroyer.expects(:destroy)
|
destroyer.expects(:destroy)
|
||||||
xhr :delete, :destroy, id: post.id
|
xhr :delete, :destroy, id: post.id
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,6 +33,42 @@ describe StaffActionLogger do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'log_post_deletion' do
|
||||||
|
let(:deleted_post) { Fabricate(:post) }
|
||||||
|
|
||||||
|
subject(:log_post_deletion) { described_class.new(admin).log_post_deletion(deleted_post) }
|
||||||
|
|
||||||
|
it 'raises an error when post is nil' do
|
||||||
|
expect { logger.log_post_deletion(nil) }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error when post is not a Post' do
|
||||||
|
expect { logger.log_post_deletion(1) }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new UserHistory record' do
|
||||||
|
expect { log_post_deletion }.to change { UserHistory.count }.by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'log_topic_deletion' do
|
||||||
|
let(:deleted_topic) { Fabricate(:topic) }
|
||||||
|
|
||||||
|
subject(:log_topic_deletion) { described_class.new(admin).log_topic_deletion(deleted_topic) }
|
||||||
|
|
||||||
|
it 'raises an error when topic is nil' do
|
||||||
|
expect { logger.log_topic_deletion(nil) }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error when topic is not a Topic' do
|
||||||
|
expect { logger.log_topic_deletion(1) }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new UserHistory record' do
|
||||||
|
expect { log_topic_deletion }.to change { UserHistory.count }.by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'log_trust_level_change' do
|
describe 'log_trust_level_change' do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:old_trust_level) { TrustLevel[0] }
|
let(:old_trust_level) { TrustLevel[0] }
|
||||||
|
|
Loading…
Reference in a new issue