FEATURE: flag dispositions normalization

All flags should end up in one of the three dispositions
  - Agree
  - Disagree
  - Defer

In the administration area, the *active* flags section displays 4 buttons
  - Agree (hide post + send PM)
  - Disagree
  - Defer
  - Delete

Clicking "Delete" will open a modal that offer to
  - Delete Post & Defer Flags
  - Delete Post & Agree with Flags
  - Delete Spammer (if available)

When the flag has a list associated, the list will now display 1
response and 1 reply and a "show more..." link if there are more in the
conversation. Replying to the conversation will NOT give a disposition.
Moderators must click the buttons that does that.

If someone clicks one buttons, this will add a default moderator message
from that moderator saying what happened.

The *old* flags section now displays the proper dispositions and is
super duper fast (no more N+9999 queries).

FIX: the old list includes deleted topics
FIX: the lists now properly display the topic states (deleted, closed,
archived, hidden, PM)
FIX: flagging a topic that you've already flagged the first post
This commit is contained in:
Régis Hanol 2014-07-28 19:17:37 +02:00
parent 717f57c968
commit bddffa7f9a
50 changed files with 886 additions and 558 deletions

View File

@ -8,36 +8,34 @@
**/
export default Ember.ArrayController.extend({
adminOldFlagsView: Em.computed.equal('query', 'old'),
adminActiveFlagsView: Em.computed.equal('query', 'active'),
actions: {
/**
Clear all flags on a post
@method clearFlags
@param {Discourse.FlaggedPost} item The post whose flags we want to clear
**/
disagreeFlags: function(item) {
var adminFlagsController = this;
item.disagreeFlags().then(function() {
adminFlagsController.removeObject(item);
}, function() {
agreeFlags: function (flaggedPost) {
var self = this;
flaggedPost.agreeFlags().then(function () {
self.removeObject(flaggedPost);
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
agreeFlags: function(item) {
var adminFlagsController = this;
item.agreeFlags().then(function() {
adminFlagsController.removeObject(item);
}, function() {
disagreeFlags: function (flaggedPost) {
var self = this;
flaggedPost.disagreeFlags().then(function () {
self.removeObject(flaggedPost);
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
deferFlags: function(item) {
var adminFlagsController = this;
item.deferFlags().then(function() {
adminFlagsController.removeObject(item);
}, function() {
deferFlags: function (flaggedPost) {
var self = this;
flaggedPost.deferFlags().then(function () {
self.removeObject(flaggedPost);
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
@ -45,47 +43,8 @@ export default Ember.ArrayController.extend({
doneTopicFlags: function(item) {
this.send('disagreeFlags', item);
},
/**
Deletes a post
@method deletePost
@param {Discourse.FlaggedPost} post The post to delete
**/
deletePost: function(post) {
var adminFlagsController = this;
post.deletePost().then(function() {
adminFlagsController.removeObject(post);
}, function() {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
/**
Deletes a user and all posts and topics created by that user.
@method deleteSpammer
@param {Discourse.FlaggedPost} item The post to delete
**/
deleteSpammer: function(item) {
item.get('user').deleteAsSpammer(function() { window.location.reload(); });
}
},
/**
Are we viewing the 'old' view?
@property adminOldFlagsView
**/
adminOldFlagsView: Em.computed.equal('query', 'old'),
/**
Are we viewing the 'active' view?
@property adminActiveFlagsView
**/
adminActiveFlagsView: Em.computed.equal('query', 'active'),
loadMore: function(){
var flags = this.get('model');
return Discourse.FlaggedPost.findAll(this.get('query'),flags.length+1).then(function(data){

View File

@ -0,0 +1,52 @@
/**
The modal for deleting a flag.
@class AdminDeleteFlagController
@extends Discourse.Controller
@namespace Discourse
@uses Discourse.ModalFunctionality
@module Discourse
**/
Discourse.AdminDeleteFlagController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
needs: ["adminFlags"],
actions: {
deletePostDeferFlag: function () {
var adminFlagController = this.get("controllers.adminFlags");
var post = this.get("content");
var self = this;
return post.deferFlags(true).then(function () {
adminFlagController.removeObject(post);
self.send("closeModal");
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
deletePostAgreeFlag: function () {
var adminFlagController = this.get("controllers.adminFlags");
var post = this.get("content");
var self = this;
return post.agreeFlags(true).then(function () {
adminFlagController.removeObject(post);
self.send("closeModal");
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
/**
Deletes a user and all posts and topics created by that user.
@method deleteSpammer
**/
deleteSpammer: function () {
this.get("content.user").deleteAsSpammer(function() { window.location.reload(); });
}
}
});

View File

@ -8,64 +8,69 @@
**/
Discourse.FlaggedPost = Discourse.Post.extend({
summary: function(){
summary: function () {
return _(this.post_actions)
.groupBy(function(a){ return a.post_action_type_id; })
.map(function(v,k){
return I18n.t('admin.flags.summary.action_type_' + k, {count: v.length});
})
.groupBy(function (a) { return a.post_action_type_id; })
.map(function (v,k) { return I18n.t('admin.flags.summary.action_type_' + k, { count: v.length }); })
.join(',');
}.property(),
flaggers: function() {
var r,
_this = this;
r = [];
_.each(this.post_actions, function(action) {
var user = _this.userLookup[action.user_id];
var deletedBy = null;
if(action.deleted_by_id){
deletedBy = _this.userLookup[action.deleted_by_id];
}
flaggers: function () {
var self = this;
var flaggers = [];
var flagType = I18n.t('admin.flags.summary.action_type_' + action.post_action_type_id, {count: 1});
r.push({
user: user, flagType: flagType, flaggedAt: action.created_at, deletedBy: deletedBy,
tookAction: action.staff_took_action, deletedAt: action.deleted_at
_.each(this.post_actions, function (postAction) {
flaggers.push({
user: self.userLookup[postAction.user_id],
topic: self.topicLookup[postAction.topic_id],
flagType: I18n.t('admin.flags.summary.action_type_' + postAction.post_action_type_id, { count: 1 }),
flaggedAt: postAction.created_at,
disposedBy: postAction.disposed_by_id ? self.userLookup[postAction.disposed_by_id] : null,
disposedAt: postAction.disposed_at,
disposition: postAction.disposition ? I18n.t('admin.flags.dispositions.' + postAction.disposition) : null,
tookAction: postAction.staff_took_action
});
});
return r;
return flaggers;
}.property(),
messages: function() {
var r,
_this = this;
r = [];
_.each(this.post_actions,function(action) {
if (action.message) {
r.push({
user: _this.userLookup[action.user_id],
message: action.message,
permalink: action.permalink,
bySystemUser: (action.user_id === -1 ? true : false)
});
conversations: function () {
var self = this;
var conversations = [];
_.each(this.post_actions, function (postAction) {
if (postAction.conversation) {
var conversation = {
permalink: postAction.permalink,
hasMore: postAction.conversation.has_more,
response: {
excerpt: postAction.conversation.response.excerpt,
user: self.userLookup[postAction.conversation.response.user_id]
}
};
if (postAction.conversation.reply) {
conversation["reply"] = {
excerpt: postAction.conversation.reply.excerpt,
user: self.userLookup[postAction.conversation.reply.user_id]
};
}
conversations.push(conversation);
}
});
return r;
}.property(),
lastFlagged: function() {
return this.post_actions[0].created_at;
return conversations;
}.property(),
user: function() {
return this.userLookup[this.user_id];
}.property(),
topicHidden: function() {
return !this.get('topic_visible');
}.property('topic_hidden'),
topic: function () {
return this.topicLookup[this.topic_id];
}.property(),
flaggedForSpam: function() {
return !_.every(this.get('post_actions'), function(action) { return action.name_key !== 'spam'; });
@ -80,7 +85,7 @@ Discourse.FlaggedPost = Discourse.Post.extend({
}.property('post_actions.@each.targets_topic'),
canDeleteAsSpammer: function() {
return (Discourse.User.currentProp('staff') && this.get('flaggedForSpam') && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted'));
return Discourse.User.currentProp('staff') && this.get('flaggedForSpam') && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted');
}.property('flaggedForSpam'),
deletePost: function() {
@ -91,28 +96,24 @@ Discourse.FlaggedPost = Discourse.Post.extend({
}
},
disagreeFlags: function() {
disagreeFlags: function () {
return Discourse.ajax('/admin/flags/disagree/' + this.id, { type: 'POST', cache: false });
},
deferFlags: function() {
return Discourse.ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false });
deferFlags: function (deletePost) {
return Discourse.ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } });
},
agreeFlags: function() {
return Discourse.ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false });
agreeFlags: function (deletePost) {
return Discourse.ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } });
},
postHidden: Em.computed.alias('hidden'),
extraClasses: function() {
var classes = [];
if (this.get('hidden')) {
classes.push('hidden-post');
}
if (this.get('deleted')){
classes.push('deleted');
}
if (this.get('hidden')) { classes.push('hidden-post'); }
if (this.get('deleted')) { classes.push('deleted'); }
return classes.join(' ');
}.property(),
@ -121,26 +122,36 @@ Discourse.FlaggedPost = Discourse.Post.extend({
});
Discourse.FlaggedPost.reopenClass({
findAll: function(filter, offset) {
findAll: function (filter, offset) {
offset = offset || 0;
var result = Em.A();
result.set('loading', true);
return Discourse.ajax('/admin/flags/' + filter + '.json?offset=' + offset).then(function(data) {
return Discourse.ajax('/admin/flags/' + filter + '.json?offset=' + offset).then(function (data) {
// users
var userLookup = {};
_.each(data.users,function(user) {
_.each(data.users,function (user) {
userLookup[user.id] = Discourse.AdminUser.create(user);
});
_.each(data.posts,function(post) {
// topics
var topicLookup = {};
_.each(data.topics, function (topic) {
topicLookup[topic.id] = Discourse.Topic.create(topic);
});
// posts
_.each(data.posts,function (post) {
var f = Discourse.FlaggedPost.create(post);
f.userLookup = userLookup;
f.topicLookup = topicLookup;
result.pushObject(f);
});
result.set('loading', false);
return result;
});
}
});

View File

@ -18,7 +18,16 @@ Discourse.AdminFlagsRouteType = Discourse.Route.extend({
});
Discourse.AdminFlagsActiveRoute = Discourse.AdminFlagsRouteType.extend({
filter: 'active'
filter: 'active',
actions: {
showDeleteFlagModal: function(flaggedPost) {
Discourse.Route.showModal(this, 'admin_delete_flag', flaggedPost);
this.controllerFor('modal').set('modalClass', 'delete-flag-modal');
}
}
});

View File

@ -8,10 +8,10 @@
</div>
<div class="admin-container">
{{#if model.loading}}
{{#if loading}}
<div class='admin-loading'>{{i18n loading}}</div>
{{else}}
{{#if model.length}}
{{#if length}}
<table class='admin-flags'>
<thead>
<tr>
@ -19,131 +19,141 @@
<th class='excerpt'></th>
<th class='flaggers'>{{i18n admin.flags.flagged_by}}</th>
<th class='flaggers'>{{#if adminOldFlagsView}}{{i18n admin.flags.resolved_by}}{{/if}}</th>
<th class='last-flagged'></th>
<th class='action'></th>
</tr>
</thead>
<tbody>
{{#each flaggedPost in content}}
<tr {{bind-attr class="flaggedPost.extraClasses"}}>
<tr {{bind-attr class="flaggedPost.extraClasses"}}>
<td class='user'>
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.user}}
{{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}}
<td class='user'>
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.user}}
{{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}}
{{/if}}
{{/if}}
{{/if}}
</td>
</td>
<td class='excerpt'>
{{#if flaggedPost.topicHidden}}<i title='{{i18n topic_statuses.invisible.help}}' class='fa fa-eye-slash'></i> {{/if}}<h3><a href='{{unbound flaggedPost.url}}'>{{flaggedPost.title}}</a></h3>
<br>
{{#if flaggedPost.postAuthorFlagged}}
{{{flaggedPost.excerpt}}}
{{/if}}
</td>
<td class='excerpt'>
<h3>
{{#if flaggedPost.topic.isPrivateMessage}}
<span class="private-message-glyph">{{icon envelope}}</span>
{{/if}}
{{topic-status topic=flaggedPost.topic}}
<a href='{{unbound flaggedPost.topic.url}}'>{{flaggedPost.topic.title}}</a>
</h3>
{{#if flaggedPost.postAuthorFlagged}}
{{{flaggedPost.excerpt}}}
{{/if}}
</td>
<td class='flaggers'>
<table>
<tbody>
{{#each flaggedPost.flaggers}}
<tr>
<td>
{{#link-to 'adminUser' this.user}}{{avatar this.user imageSize="small"}} {{/link-to}}
</td>
<td>
{{date this.flaggedAt}}
</td>
<td>
{{this.flagType}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</td>
<td class='flaggers'>
<table>
<tbody>
{{#each flaggedPost.flaggers}}
<tr>
<td width="20%">
{{#link-to 'adminUser' user}}
{{avatar user imageSize="small"}}
{{/link-to}}
</td>
<td width="30%">
{{date flaggedAt}}
</td>
<td width="50%">
{{flagType}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</td>
<td class='flaggers result'>
<table>
<tbody>
{{#each flaggedPost.flaggers}}
<tr>
{{#if deletedBy}}
<td>
{{#link-to 'adminUser' this.deletedBy}}{{avatar this.deletedBy imageSize="small"}} {{/link-to}}
</td>
<td>
{{#if this.tookAction}}
<i class='fa fa-gavel'></i>
{{/if}}
</td>
<td>
{{date this.deletedAt}}
</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
</td>
</tr>
<td class='flaggers result'>
<table>
<tbody>
{{#each flaggedPost.flaggers}}
<tr>
<td width="20%">
{{#link-to 'adminUser' disposedBy}}
{{avatar disposedBy imageSize="small"}}
{{/link-to}}
</td>
<td width="30%">
{{date disposedAt}}
</td>
<td width="50%">
{{disposition}}
{{#if tookAction}}
<i class='fa fa-gavel'></i>
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</td>
</tr>
{{#if flaggedPost.topicFlagged}}
<tr>
<td></td>
<td class='message'><div>{{{i18n admin.flags.topic_flagged}}}</div></td>
<td></td>
<tr class='message'>
<td></td>
<td colspan="3">
<div>
{{{i18n admin.flags.topic_flagged}}}
</div>
</td>
</tr>
{{/if}}
{{#each flaggedPost.messages}}
<tr>
{{#each flaggedPost.conversations}}
<tr class='message'>
<td></td>
<td class='message'>
<td colspan="3">
<div>
{{#unless bySystemUser}}
{{#link-to 'adminUser' user}}{{avatar user imageSize="small"}}{{/link-to}}
{{message}}
<a href="{{unbound permalink}}"><button class='btn'><i class="fa fa-reply"></i> {{i18n admin.flags.view_message}}</button></a>
{{else}}
<b>{{i18n admin.flags.system}}</b>:
{{message}}
{{/unless}}
{{#if response}}
<p>
{{#link-to 'adminUser' response.user}}{{avatar response.user imageSize="small"}}{{/link-to}}&nbsp;{{{response.excerpt}}}
</p>
{{#if reply}}
<p>
{{#link-to 'adminUser' reply.user}}{{avatar reply.user imageSize="small"}}{{/link-to}}&nbsp;{{{reply.excerpt}}}
{{#if hasMore}}
<a href="{{unbound permalink}}">{{i18n admin.flags.more}}</a>
{{/if}}
</p>
{{/if}}
<a href="{{unbound permalink}}">
<button class='btn btn-reply'><i class="fa fa-reply"></i>&nbsp;{{i18n admin.flags.reply_message}}</button>
</a>
{{/if}}
</div>
</td>
<td></td>
<td></td>
</tr>
{{/each}}
<tr>
<tr>
<td colspan="4" class="action">
{{#if adminActiveFlagsView}}
{{#if flaggedPost.topicFlagged}}
<a href='{{unbound flaggedPost.url}}' class="btn">{{i18n admin.flags.visit_topic}}</a>
{{/if}}
{{#if adminActiveFlagsView}}
{{#if flaggedPost.topicFlagged}}
<a href='{{unbound flaggedPost.url}}' class="btn">{{i18n admin.flags.visit_topic}}</a>
{{/if}}
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.postHidden}}
<button title='{{i18n admin.flags.disagree_unhide_title}}' class='btn' {{action disagreeFlags flaggedPost}}><i class="fa fa-thumbs-o-down"></i> {{i18n admin.flags.disagree_unhide}}</button>
<button title='{{i18n admin.flags.defer_title}}' class='btn' {{action deferFlags flaggedPost}}><i class="fa fa-external-link"></i> {{i18n admin.flags.defer}}</button>
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.postHidden}}
<button title='{{i18n admin.flags.disagree_flag_unhide_post_title}}' class='btn' {{action disagreeFlags flaggedPost}}><i class="fa fa-thumbs-o-down"></i>&nbsp;{{i18n admin.flags.disagree_flag_unhide_post}}</button>
{{else}}
<button title='{{i18n admin.flags.agree_flag_hide_post_title}}' class='btn btn-primary' {{action agreeFlags flaggedPost}}><i class="fa fa-thumbs-o-up"></i>&nbsp;{{i18n admin.flags.agree_flag_hide_post}}</button>
<button title='{{i18n admin.flags.disagree_flag_title}}' class='btn' {{action disagreeFlags flaggedPost}}><i class="fa fa-thumbs-o-down"></i>&nbsp;{{i18n admin.flags.disagree_flag}}</button>
{{/if}}
<button title='{{i18n admin.flags.defer_flag_title}}' class='btn' {{action deferFlags flaggedPost}}><i class="fa fa-external-link"></i>&nbsp;{{i18n admin.flags.defer_flag}}</button>
<button title='{{i18n admin.flags.delete_title}}' class='btn btn-danger' {{action showDeleteFlagModal flaggedPost}}><i class="fa fa-trash-o"></i>&nbsp;{{i18n admin.flags.delete}}</button>
{{else}}
<button title='{{i18n admin.flags.agree_hide_title}}' class='btn' {{action agreeFlags flaggedPost}}><i class="fa fa-thumbs-o-up"></i> {{i18n admin.flags.agree_hide}}</button>
<button title='{{i18n admin.flags.disagree_title}}' class='btn' {{action disagreeFlags flaggedPost}}><i class="fa fa-thumbs-o-down"></i> {{i18n admin.flags.disagree}}</button>
<button title='{{i18n admin.flags.clear_topic_flags_title}}' class='btn' {{action doneTopicFlags flaggedPost}}>{{i18n admin.flags.clear_topic_flags}}</button>
{{/if}}
{{#if flaggedPost.canDeleteAsSpammer}}
<button title='{{i18n admin.flags.delete_spammer_title}}' class="btn" {{action deleteSpammer flaggedPost}}><i class="fa fa-exclamation-triangle"></i> {{i18n flagging.delete_spammer}}</button>
{{/if}}
<button title='{{i18n admin.flags.delete_post_title}}' class='btn' {{action deletePost flaggedPost}}><i class="fa fa-trash-o"></i> {{i18n admin.flags.delete_post}}</button>
{{else}}
<button title='{{i18n admin.flags.clear_topic_flags_title}}' class='btn' {{action doneTopicFlags flaggedPost}}>{{i18n admin.flags.clear_topic_flags}}</button>
{{/if}}
{{/if}}
</td>
</tr>
</tr>
{{/each}}

View File

@ -0,0 +1,5 @@
<button title="{{i18n admin.flags.delete_post_defer_flag_title}}" {{action deletePostDeferFlag}} class="btn btn-primary"><i class="fa fa-trash-o"></i> {{i18n admin.flags.delete_post_defer_flag}}</button>
<button title="{{i18n admin.flags.delete_post_agree_flag_title}}" {{action deletePostAgreeFlag}} class="btn btn-primary"><i class="fa fa-trash-o"></i> {{i18n admin.flags.delete_post_agree_flag}}</button>
{{#if canDeleteAsSpammer}}
<button title="{{i18n admin.flags.delete_spammer_title}}" {{action deleteSpammer}} class="btn btn-danger"><i class="fa fa-exclamation-triangle"></i> {{i18n admin.flags.delete_spammer}}</button>
{{/if}}

View File

@ -1,13 +1,21 @@
Discourse.AdminFlagsView = Discourse.View.extend(Discourse.LoadMore, {
loading: false,
eyelineSelector: '.admin-flags tbody tr',
loadMore: function() {
var view = this;
if(this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
this.get("controller").loadMore().then(function(){
view.set("loading", false);
});
actions: {
loadMore: function() {
var self = this;
if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
this.get("controller").loadMore().then(function () {
self.set("loading", false);
});
}
}
});

View File

@ -0,0 +1,12 @@
/**
A modal view for deleting a flag.
@class AdminDeleteFlagView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AdminDeleteFlagView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_delete_flag',
title: I18n.t('admin.flags.delete_flag_modal_title')
});

View File

@ -53,7 +53,7 @@ export default Em.Component.extend({
renderActionIf('usersCollapsed', 'who-acted', c.get('description'));
renderActionIf('canAlsoAction', 'act', I18n.t("post.actions.it_too." + c.get('actionType.name_key')));
renderActionIf('can_undo', 'undo', I18n.t("post.actions.undo." + c.get('actionType.name_key')));
renderActionIf('can_clear_flags', 'clear-flags', I18n.t("post.actions.clear_flags", { count: c.count }));
renderActionIf('can_defer_flags', 'defer-flags', I18n.t("post.actions.defer_flags", { count: c.count }));
buffer.push("</div>");
});
@ -77,8 +77,8 @@ export default Em.Component.extend({
var $target = $(e.target),
actionTypeId;
if (actionTypeId = $target.data('clear-flags')) {
this.actionTypeById(actionTypeId).clearFlags();
if (actionTypeId = $target.data('defer-flags')) {
this.actionTypeById(actionTypeId).deferFlags();
return false;
}

View File

@ -47,9 +47,7 @@ export default Ember.Component.extend({
// Allow a plugin to add a custom icon to a topic
this.trigger('addCustomIcon', buffer);
var togglePin = function(){
};
var togglePin = function () {};
renderIconIf('topic.closed', 'lock', 'locked');
renderIconIf('topic.archived', 'lock', 'archived');

View File

@ -68,7 +68,7 @@ Discourse.ActionSummary = Discourse.Model.extend({
if(action === 'notify_moderators' || action === 'notify_user') {
this.set('can_undo',false);
this.set('can_clear_flags',false);
this.set('can_defer_flags',false);
}
// Add ourselves to the users who liked it if present
@ -108,9 +108,9 @@ Discourse.ActionSummary = Discourse.Model.extend({
});
},
clearFlags: function() {
deferFlags: function() {
var actionSummary = this;
return Discourse.ajax("/post_actions/clear_flags", {
return Discourse.ajax("/post_actions/defer_flags", {
type: "POST",
data: {
post_action_type_id: this.get('id'),

View File

@ -13,20 +13,20 @@
{{#if topic.isPrivateMessage}}
<span class="private-message-glyph">{{icon envelope}}</span>
{{/if}}
{{#if topic.category.parentCategory}}
{{bound-category-link topic.category.parentCategory}}
{{/if}}
{{bound-category-link topic.category}}
{{#if topic.details.loaded}}
{{topic-status topic=topic}}
<a class='topic-link' href='{{unbound topic.url}}' {{action jumpToTopPost}}>{{{topic.fancy_title}}}</a>
{{else}}
{{#if topic.errorLoading}}
{{topic.errorTitle}}
{{else}}
{{i18n topic.loading}}
{{#if topic.category.parentCategory}}
{{bound-category-link topic.category.parentCategory}}
{{/if}}
{{bound-category-link topic.category}}
{{#if topic.details.loaded}}
{{topic-status topic=topic}}
<a class='topic-link' href='{{unbound topic.url}}' {{action jumpToTopPost}}>{{{topic.fancy_title}}}</a>
{{else}}
{{#if topic.errorLoading}}
{{topic.errorTitle}}
{{else}}
{{i18n topic.loading}}
{{/if}}
{{/if}}
{{/if}}
</h1>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{#if editingTopic}}
{{#if isPrivateMessage}}
<span class="private-message-glyph"><i class='fa fa-envelope'></i></span>
<span class="private-message-glyph">{{icon envelope}}</span>
{{else}}
{{category-chooser valueAttribute="id" value=newCategoryId source=category_id}}
{{/if}}

View File

@ -494,25 +494,30 @@ section.details {
.admin-flags {
tr.hidden-post td.excerpt { opacity: 0.4; }
tr.deleted td.excerpt { opacity: 0.8; background-color: scale-color($danger, $lightness: 30%); }
td.message {
padding: 4px 8px;
background-color: scale-color($highlight, $lightness: 30%);
}
.hidden-post td.excerpt { opacity: 0.5; }
.deleted td.excerpt { background-color: scale-color($danger, $lightness: 70%); }
.message { background-color: scale-color($highlight, $lightness: 70%); }
.message:hover { background-color: scale-color($highlight, $lightness: 30%); }
td { vertical-align: top; }
th { text-align: left; }
.user { width: 40px; padding-top: 12px; }
.user {
width: 20px;
padding-top: 8px;
}
.excerpt {
max-width: 740px;
width: 740px;
max-width: 720px;
width: 720px;
padding: 8px;
word-wrap: break-word;
.fa,h3 { display: inline-block; }
.fa { display: inline-block; }
h3 {
max-height: 1.2em;
overflow: hidden;
}
}
.flaggers {
font-size: 11px;
padding: 8px 0 0 5px;
td {
vertical-align: middle;
padding: 3px;
@ -523,6 +528,10 @@ section.details {
text-align: right;
padding-bottom: 20px;
}
td p {
font-size: 13px;
margin: 0 0 5px 0;
}
}
/* Dashboard */
@ -1135,6 +1144,17 @@ button.ru {
visibility: hidden;
}
.delete-flag-modal {
.modal-inner-container {
width: 400px;
}
button {
display: block;
margin: 10px 0 10px 10px;
padding: 10px 15px;
}
}
@media all
and (max-width : 850px) {
.nav-stacked {
@ -1202,7 +1222,6 @@ and (max-width : 500px) {
.customize .content-list, .customize .current-style {
width: 100%;
}
}

View File

@ -1,37 +1,50 @@
require 'flag_query'
class Admin::FlagsController < Admin::AdminController
def index
# we may get out of sync, fix it here
PostAction.update_flagged_posts_count
posts, users = FlagQuery.flagged_posts_report(current_user, params[:filter], params[:offset].to_i, 10)
posts, topics, users = FlagQuery.flagged_posts_report(current_user, params[:filter], params[:offset].to_i, 10)
if posts.blank?
render json: {users: [], posts: []}
render json: { posts: [], topics: [], users: [] }
else
render json: MultiJson.dump({users: serialize_data(users, AdminDetailedUserSerializer), posts: posts})
render json: MultiJson.dump({
posts: posts,
topics: serialize_data(topics, FlaggedTopicSerializer),
users: serialize_data(users, FlaggedUserSerializer)
})
end
end
def disagree
p = Post.find(params[:id])
PostAction.clear_flags!(p, current_user.id)
p.reload
p.unhide!
def agree
params.permit(:id, :delete_post)
post = Post.find(params[:id])
post_action_type = PostAction.post_action_type_for_post(post.id)
PostAction.agree_flags!(post, current_user, params[:delete_post])
if params[:delete_post]
PostDestroyer.new(current_user, post).destroy
else
PostAction.hide_post!(post, post_action_type)
end
render nothing: true
end
def agree
p = Post.find(params[:id])
post_action_type = PostAction.post_action_type_for_post(p.id)
PostAction.defer_flags!(p, current_user.id)
PostAction.hide_post!(p, post_action_type)
def disagree
params.permit(:id)
post = Post.find(params[:id])
PostAction.clear_flags!(post, current_user)
post.reload
post.unhide!
render nothing: true
end
def defer
p = Post.find(params[:id])
PostAction.defer_flags!(p, current_user.id)
params.permit(:id, :delete_post)
post = Post.find(params[:id])
PostAction.defer_flags!(post, current_user, params[:delete_post])
PostDestroyer.new(current_user, post).destroy if params[:delete_post]
render nothing: true
end

View File

@ -181,7 +181,7 @@ class Admin::UsersController < Admin::AdminController
end
def destroy
user = User.find_by(id: params[:id])
user = User.find_by(id: params[:id].to_i)
guardian.ensure_can_delete_user!(user)
begin
if UserDestroyer.new(current_user).destroy(user, params.slice(:delete_posts, :block_email, :block_urls, :block_ip, :context))

View File

@ -11,7 +11,7 @@ class PostActionsController < ApplicationController
args = {}
args[:message] = params[:message] if params[:message].present?
args[:take_action] = true if guardian.is_staff? and params[:take_action] == 'true'
args[:take_action] = true if guardian.is_staff? && params[:take_action] == 'true'
args[:flag_topic] = true if params[:flag_topic] == 'true'
post_action = PostAction.act(current_user, @post, @post_action_type_id, args)
@ -46,17 +46,17 @@ class PostActionsController < ApplicationController
render nothing: true
end
def clear_flags
guardian.ensure_can_clear_flags!(@post)
def defer_flags
guardian.ensure_can_defer_flags!(@post)
PostAction.clear_flags!(@post, current_user.id, @post_action_type_id)
PostAction.defer_flags!(@post, current_user)
@post.reload
if @post.is_flagged?
render json: {success: true, hidden: true}
render json: { success: true, hidden: true }
else
@post.unhide!
render json: {success: true, hidden: false}
render json: { success: true, hidden: false }
end
end

View File

@ -16,17 +16,35 @@ class PostAction < ActiveRecord::Base
rate_limit :post_action_rate_limiter
scope :spam_flags, -> { where(post_action_type_id: PostActionType.types[:spam]) }
scope :flags, -> { where(post_action_type_id: PostActionType.notify_flag_type_ids) }
scope :publics, -> { where(post_action_type_id: PostActionType.public_type_ids) }
scope :active, -> { where(defered_at: nil, agreed_at: nil, deleted_at: nil) }
after_save :update_counters
after_save :enforce_rules
after_commit :notify_subscribers
def disposed_by_id
deleted_by_id || agreed_by_id || defered_by_id
end
def disposed_at
deleted_at || agreed_at || defered_at
end
def disposition
return :disagreed if deleted_at
return :agreed if agreed_at
return :defered if defered_at
nil
end
def self.update_flagged_posts_count
posts_flagged_count = PostAction.joins(post: :topic)
.where('defer = false or defer IS NULL')
.where('post_actions.post_action_type_id' => PostActionType.notify_flag_type_ids,
'posts.deleted_at' => nil,
'topics.deleted_at' => nil)
posts_flagged_count = PostAction.active
.flags
.joins(post: :topic)
.where('posts.deleted_at' => nil)
.where('topics.deleted_at' => nil)
.count('DISTINCT posts.id')
$redis.set('posts_flagged_count', posts_flagged_count)
@ -39,58 +57,93 @@ class PostAction < ActiveRecord::Base
end
def self.counts_for(collection, user)
return {} if collection.blank?
return {} if collection.blank?
collection_ids = collection.map {|p| p.id}
collection_ids = collection.map(&:id)
user_id = user.present? ? user.id : 0
result = PostAction.where(post_id: collection_ids, user_id: user_id)
post_actions = PostAction.where(post_id: collection_ids, user_id: user_id)
user_actions = {}
result.each do |r|
user_actions[r.post_id] ||= {}
user_actions[r.post_id][r.post_action_type_id] = r
post_actions.each do |post_action|
user_actions[post_action.post_id] ||= {}
user_actions[post_action.post_id][post_action.post_action_type_id] = post_action
end
user_actions
end
def self.count_per_day_for_type(sinceDaysAgo = 30, post_action_type)
unscoped.where(post_action_type_id: post_action_type).where('created_at > ?', sinceDaysAgo.days.ago).group('date(created_at)').order('date(created_at)').count
def self.count_per_day_for_type(post_action_type, since_days_ago=30)
unscoped.where(post_action_type_id: post_action_type)
.where('created_at > ?', since_days_ago.days.ago)
.group('date(created_at)')
.order('date(created_at)')
.count
end
def self.clear_flags!(post, moderator_id, action_type_id = nil)
# -1 is the automatic system cleary
actions = if action_type_id
[action_type_id]
else
moderator_id == -1 ? PostActionType.auto_action_flag_types.values : PostActionType.flag_types.values
end
def self.agree_flags!(post, moderator, delete_post=false)
actions = PostAction.active
.where(post_id: post.id)
.where(post_action_type_id: PostActionType.flag_types.values)
PostAction.where({ post_id: post.id, post_action_type_id: actions }).update_all({ deleted_at: Time.zone.now, deleted_by_id: moderator_id })
f = actions.map{|t| ["#{PostActionType.types[t]}_count", 0]}
Post.where(id: post.id).with_deleted.update_all(Hash[*f.flatten])
update_flagged_posts_count
end
def self.defer_flags!(post, moderator_id)
actions = PostAction.where(
defer: nil,
post_id: post.id,
post_action_type_id: PostActionType.flag_types.values,
deleted_at: nil
)
actions.each do |a|
a.defer = true
a.defer_by = moderator_id
actions.each do |action|
action.agreed_at = Time.zone.now
action.agreed_by_id = moderator.id
# so callback is called
a.save
action.save
action.add_moderator_post_if_needed(moderator, :agreed, delete_post)
end
update_flagged_posts_count
end
def self.clear_flags!(post, moderator)
# -1 is the automatic system cleary
action_type_ids = moderator.id == -1 ?
PostActionType.auto_action_flag_types.values :
PostActionType.flag_types.values
actions = PostAction.where(post_id: post.id)
.where(post_action_type_id: action_type_ids)
actions.each do |action|
action.deleted_at = Time.zone.now
action.deleted_by_id = moderator.id
# so callback is called
action.save
action.add_moderator_post_if_needed(moderator, :disagreed)
end
# reset all cached counters
f = action_type_ids.map { |t| ["#{PostActionType.types[t]}_count", 0] }
Post.with_deleted.where(id: post.id).update_all(Hash[*f.flatten])
update_flagged_posts_count
end
def self.defer_flags!(post, moderator, delete_post=false)
actions = PostAction.active
.where(post_id: post.id)
.where(post_action_type_id: PostActionType.flag_types.values)
actions.each do |action|
action.defered_at = Time.zone.now
action.defered_by_id = moderator.id
# so callback is called
action.save
action.add_moderator_post_if_needed(moderator, :defered, delete_post)
end
update_flagged_posts_count
end
def add_moderator_post_if_needed(moderator, disposition, delete_post=false)
return unless related_post
message_key = "flags_dispositions.#{disposition}"
message_key << "_and_deleted" if delete_post
related_post.topic.add_moderator_post(moderator, I18n.t(message_key))
end
def self.create_message_for_post_action(user, post, post_action_type_id, opts)
post_action_type = PostActionType.types[post_action_type_id]
@ -123,10 +176,10 @@ class PostAction < ActiveRecord::Base
end
def self.act(user, post, post_action_type_id, opts={})
related_post_id = create_message_for_post_action(user, post, post_action_type_id, opts)
staff_took_action = opts[:take_action] || false
related_post_id = create_message_for_post_action(user,post,post_action_type_id,opts)
targets_topic = if opts[:flag_topic] and post.topic
targets_topic = if opts[:flag_topic] && post.topic
post.topic.reload
post.topic.posts_count != 1
end
@ -138,17 +191,16 @@ class PostAction < ActiveRecord::Base
}
action_attributes = {
message: opts[:message],
staff_took_action: opts[:take_action] || false,
staff_took_action: staff_took_action,
related_post_id: related_post_id,
targets_topic: !!targets_topic
}
# First try to revive a trashed record
row_count = PostAction.where(where_attrs)
.with_deleted
.where("deleted_at IS NOT NULL")
.update_all(action_attributes.merge(deleted_at: nil))
.with_deleted
.where("deleted_at IS NOT NULL")
.update_all(action_attributes.merge(deleted_at: nil))
if row_count == 0
post_action = create(where_attrs.merge(action_attributes))
@ -157,9 +209,13 @@ class PostAction < ActiveRecord::Base
end
else
post_action = PostAction.where(where_attrs).first
post_action.update_counters
end
# agree with other flags
PostAction.agree_flags!(post, user) if staff_took_action
# update counters
post_action.try(:update_counters)
post_action
rescue ActiveRecord::RecordNotUnique
# can happen despite being .create
@ -216,10 +272,11 @@ class PostAction < ActiveRecord::Base
before_create do
post_action_type_ids = is_flag? ? PostActionType.flag_types.values : post_action_type_id
raise AlreadyActed if PostAction.where(user_id: user_id,
post_id: post_id,
post_action_type_id: post_action_type_ids,
deleted_at: nil)
raise AlreadyActed if PostAction.where(user_id: user_id)
.where(post_id: post_id)
.where(post_action_type_id: post_action_type_ids)
.where(deleted_at: nil)
.where(targets_topic: targets_topic)
.exists?
end
@ -251,30 +308,30 @@ class PostAction < ActiveRecord::Base
PostActionType.types[post_action_type_id]
end
def update_counters
# Update denormalized counts
column = "#{post_action_type_key.to_s}_count"
delta = deleted_at.nil? ? 1 : -1
count = PostAction.where(post_id: post_id)
.where(post_action_type_id: post_action_type_id)
.count
# We probably want to refactor this method to something cleaner.
case post_action_type_key
when :vote
# Voting also changes the sort_order
Post.where(id: post_id).update_all ["vote_count = vote_count + :delta, sort_order = :max - (vote_count + :delta)",
delta: delta,
max: Topic.max_sort_order]
Post.where(id: post_id).update_all ["vote_count = :count, sort_order = :max - :count", count: count, max: Topic.max_sort_order]
when :like
# `like_score` is weighted higher for staff accounts
Post.where(id: post_id).update_all ["like_count = like_count + :delta, like_score = like_score + :score_delta",
delta: delta,
score_delta: user.staff? ? delta * SiteSetting.staff_like_weight : delta]
score = PostAction.joins(:user)
.where(post_id: post_id)
.sum("CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END")
Post.where(id: post_id).update_all ["like_count = :count, like_score = :score", count: count, score: score]
else
Post.where(id: post_id).update_all ["#{column} = #{column} + ?", delta]
Post.where(id: post_id).update_all ["#{column} = ?", count]
end
post = Post.with_deleted.where(id: post_id).first
Topic.where(id: post.topic_id).update_all ["#{column} = #{column} + ?", delta]
topic_id = Post.with_deleted.where(id: post_id).pluck(:topic_id).first
Topic.where(id: topic_id).update_all ["#{column} = ?", count]
if PostActionType.notify_flag_type_ids.include?(post_action_type_id)
PostAction.update_flagged_posts_count
@ -314,7 +371,6 @@ class PostAction < ActiveRecord::Base
end
end
def self.hide_post!(post, post_action_type, reason=nil)
return if post.hidden
@ -324,8 +380,7 @@ class PostAction < ActiveRecord::Base
end
Post.where(id: post.id).update_all(["hidden = true, hidden_at = CURRENT_TIMESTAMP, hidden_reason_id = COALESCE(hidden_reason_id, ?)", reason])
Topic.where(["id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)",
topic_id: post.topic_id]).update_all(visible: false)
Topic.where(["id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", topic_id: post.topic_id]).update_all(visible: false)
# inform user
if post.user
@ -345,7 +400,7 @@ class PostAction < ActiveRecord::Base
end
def self.post_action_type_for_post(post_id)
post_action = PostAction.find_by(defer: nil, post_id: post_id, post_action_type_id: PostActionType.flag_types.values, deleted_at: nil)
post_action = PostAction.find_by(defered_at: nil, post_id: post_id, post_action_type_id: PostActionType.flag_types.values, deleted_at: nil)
PostActionType.types[post_action.post_action_type_id]
end
@ -366,15 +421,17 @@ end
# user_id :integer not null
# post_action_type_id :integer not null
# deleted_at :datetime
# created_at :datetime
# updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# deleted_by_id :integer
# message :text
# related_post_id :integer
# staff_took_action :boolean default(FALSE), not null
# defer :boolean
# defer_by :integer
# defered_at :datetime
# defer_by_id :integer
# targets_topic :boolean default(FALSE)
# agreed_at :datetime
# agreed_by_id :integer
#
# Indexes
#

View File

@ -19,6 +19,10 @@ class PostActionType < ActiveRecord::Base
@public_types ||= types.except(*flag_types.keys << :notify_user)
end
def public_type_ids
@public_type_ids ||= public_types.values
end
def flag_types
@flag_types ||= types.only(:off_topic, :spam, :inappropriate, :notify_moderators)
end

View File

@ -115,8 +115,8 @@ class Report
def self.post_action_report(report, post_action_type)
report.data = []
PostAction.count_per_day_for_type(30, post_action_type).each do |date, count|
report.data << {x: date, y: count}
PostAction.count_per_day_for_type(post_action_type).each do |date, count|
report.data << { x: date, y: count }
end
query = PostAction.unscoped.where(post_action_type_id: post_action_type)
report.total = query.count

View File

@ -477,7 +477,6 @@ class Topic < ActiveRecord::Base
topic_id: self.id)
new_post = creator.create
increment!(:moderator_posts_count)
new_post
end
if new_post.present?

View File

@ -182,9 +182,12 @@ class User < ActiveRecord::Base
end
def created_topic_count
topics.count
stat = user_stat || create_user_stat
stat.topic_count
end
alias_method :topic_count, :created_topic_count
# tricky, we need our bus to be subscribed from the right spot
def sync_notification_channel_position
@unread_notifications_by_type = nil
@ -370,11 +373,8 @@ class User < ActiveRecord::Base
end
def post_count
posts.count
end
def first_post
posts.order('created_at ASC').first
stat = user_stat || create_user_stat
stat.post_count
end
def flags_given_count
@ -607,6 +607,10 @@ class User < ActiveRecord::Base
end
end
def first_post_created_at
user_stat.try(:first_post_created_at)
end
protected
def badge_grant

View File

@ -0,0 +1,10 @@
class FlaggedTopicSerializer < ActiveModel::Serializer
attributes :id,
:title,
:slug,
:archived,
:closed,
:visible,
:archetype,
:relative_url
end

View File

@ -0,0 +1,21 @@
class FlaggedUserSerializer < BasicUserSerializer
attributes :can_delete_all_posts,
:can_be_deleted,
:post_count,
:topic_count,
:email,
:ip_address
def can_delete_all_posts
scope.can_delete_all_posts?(object)
end
def can_be_deleted
scope.can_delete_user?(object)
end
def ip_address
object.ip_address.try(:to_s)
end
end

View File

@ -164,7 +164,7 @@ class PostSerializer < BasicPostSerializer
# The following only applies if you're logged in
if action_summary[:can_act] && scope.current_user.present?
action_summary[:can_clear_flags] = scope.is_staff? && PostActionType.flag_types.values.include?(id)
action_summary[:can_defer_flags] = scope.is_staff? && PostActionType.flag_types.values.include?(id)
end
if post_actions.present? && post_actions.has_key?(id)

View File

@ -173,7 +173,7 @@ class TopicViewSerializer < ApplicationSerializer
count: 0,
hidden: false,
can_act: scope.post_can_act?(post, sym)}
# TODO: other keys? :can_clear_flags, :acted, :can_undo
# TODO: other keys? :can_defer_flags, :acted, :can_undo
end
result
end

View File

@ -1044,9 +1044,9 @@ en:
actions:
flag: 'Flag'
clear_flags:
one: "Clear flag"
other: "Clear flags"
defer_flags:
one: "Defer flag"
other: "Defer flags"
it_too:
off_topic: "Flag it too"
spam: "Flag it too"
@ -1411,25 +1411,37 @@ en:
old: "Old"
active: "Active"
agree_hide: "Agree (hide post + send PM)"
agree_hide_title: "Hide this post and automatically send the user a private message urging them to edit it"
defer: "Defer"
defer_title: "No action is necessary at this time, defer any action on this flag until a later date, or never"
delete_post: "Delete Post"
delete_post_title: "Delete post; if the first post, delete the topic"
disagree_unhide: "Disagree (unhide post)"
disagree_unhide_title: "Remove any flags from this post and make the post visible again"
disagree: "Disagree"
disagree_title: "Disagree with flag, remove any flags from this post"
agree_flag_hide_post: "Agree (hide post + send PM)"
agree_flag_hide_post_title: "Hide this post and automatically send the user a private message urging them to edit it"
defer_flag: "Defer"
defer_flag_title: "No action is necessary at this time, defer any action on this flag until a later date, or never"
delete: "Delete"
delete_title: "Delete"
delete_post_defer_flag: "Delete Post and defer flag"
delete_post_defer_flag_title: "Delete post; if the first post, delete the topic"
delete_post_agree_flag: "Delete Post and agree with flag"
delete_post_agree_flag_title: "Delete post; if the first post, delete the topic"
delete_flag_modal_title: "Choose the delete action"
delete_spammer: "Delete Spammer"
delete_spammer_title: "Delete the user and all its posts and topics."
disagree_flag_unhide_post: "Disagree (unhide post)"
disagree_flag_unhide_post_title: "Remove any flags from this post and make the post visible again"
disagree_flag: "Disagree"
disagree_flag_title: "Disagree with flag, remove any flags from this post"
clear_topic_flags: "Done"
clear_topic_flags_title: "The topic has been investigated and issues have been resolved. Click Done to remove the flags."
more: "(more...)"
dispositions:
agreed: "agreed"
disagreed: "disagreed"
defered: "defered"
flagged_by: "Flagged by"
resolved_by: "Resolved by"
system: "System"
error: "Something went wrong"
view_message: "Reply"
reply_message: "Reply"
no_results: "There are no flags."
topic_flagged: "This <strong>topic</strong> has been flagged."
visit_topic: "Visit the topic to take action"

View File

@ -1211,6 +1211,13 @@ en:
spam: "Your post was flagged as **spam**: the community thinks it is an advertisement, not useful or relevant to the topic, but promotional in nature."
notify_moderators: "Your post was flagged **for moderator attention**: the community thinks something about the post requires moderator intervention."
flags_dispositions:
agreed: "Thanks for your reporting this post. We've agreed with your flag."
agreed_and_deleted: "Thanks for your reporting this post. We've agreed with your flag and deleted the post."
disagreed: "Thanks for your reporting this post. Unfortunately, we've agreed with your flag."
defered: "Thanks for your reporting this post. We're looking into handling this post."
defered_and_deleted: "Thanks for your reporting this post. We've agreed with your flag and deleted the post."
system_messages:
post_hidden:
subject_template: "Post hidden due to community flagging"

View File

@ -273,7 +273,7 @@ Discourse::Application.routes.draw do
resources :post_actions do
collection do
get "users"
post "clear_flags"
post "defer_flags"
end
end
resources :user_actions

View File

@ -0,0 +1,6 @@
class AddAgreedAtAndAgreedByIdToPostAction < ActiveRecord::Migration
def change
add_column :post_actions, :agreed_at, :datetime
add_column :post_actions, :agreed_by_id, :integer
end
end

View File

@ -0,0 +1,17 @@
class RenameDeferColumnsOnPostAction < ActiveRecord::Migration
def up
rename_column :post_actions, :defer_by, :defered_by_id
add_column :post_actions, :defered_at, :datetime
execute "UPDATE post_actions SET defered_at = updated_at WHERE defer = 't'"
remove_column :post_actions, :defer
end
def down
rename_column :post_actions, :defered_by_id, :defer_by
add_column :post_actions, :defer, :boolean
execute "UPDATE post_actions SET defer = 't' WHERE defered_at IS NOT NULL"
remove_column :post_actions, :defered_at
end
end

View File

@ -0,0 +1,16 @@
class RemoveMessageFromPostAction < ActiveRecord::Migration
def up
remove_column :post_actions, :message
end
def down
add_column :post_actions, :message, :text
execute "UPDATE post_actions
SET message = p.raw
FROM post_actions pa
LEFT JOIN posts p ON p.id = pa.related_post_id
WHERE post_actions.id = pa.id
AND pa.related_post_id IS NOT NULL;"
end
end

View File

@ -0,0 +1,6 @@
class FixIndexOnPostAction < ActiveRecord::Migration
def change
remove_index "post_actions", name: "idx_unique_actions"
add_index "post_actions", ["user_id", "post_action_type_id", "post_id", "deleted_at", "targets_topic"], name: "idx_unique_actions", unique: true
end
end

View File

@ -0,0 +1,24 @@
class AddFirstPostCreatedAtToUserStat < ActiveRecord::Migration
def up
add_column :user_stats, :first_post_created_at, :datetime
execute <<-SQL
WITH first_posts AS (
SELECT p.id,
p.user_id,
p.created_at,
ROW_NUMBER() OVER (PARTITION BY p.user_id ORDER BY p.created_at ASC) AS row
FROM posts p
)
UPDATE user_stats us
SET first_post_created_at = fp.created_at
FROM first_posts fp
WHERE fp.row = 1
AND fp.user_id = us.user_id
SQL
end
def down
remove_column :user_stats, :first_post_created_at
end
end

View File

@ -0,0 +1,25 @@
class AddPostAndTopicCountsToUserStat < ActiveRecord::Migration
def up
add_column :user_stats, :post_count, :integer, default: 0, null: false
add_column :user_stats, :topic_count, :integer, default: 0, null: false
execute <<-SQL
UPDATE user_stats
SET post_count = pc.count
FROM (SELECT user_id, COUNT(*) AS count FROM posts GROUP BY user_id) AS pc
WHERE pc.user_id = user_stats.user_id
SQL
execute <<-SQL
UPDATE user_stats
SET topic_count = tc.count
FROM (SELECT user_id, COUNT(*) AS count FROM topics GROUP BY user_id) AS tc
WHERE tc.user_id = user_stats.user_id
SQL
end
def down
remove_column :user_stats, :post_count
remove_column :user_stats, :topic_count
end
end

View File

@ -1,105 +1,139 @@
module FlagQuery
def self.flagged_posts_report(current_user, filter, offset = 0, per_page = 25)
def self.flagged_posts_report(current_user, filter, offset=0, per_page=25)
actions = flagged_post_actions(filter)
guardian = Guardian.new(current_user)
if !guardian.is_admin?
actions = actions.joins(:post => :topic)
.where('category_id in (?)', guardian.allowed_category_ids)
actions = actions.where('category_id in (?)', guardian.allowed_category_ids)
end
post_ids = actions
.limit(per_page)
.offset(offset)
.group(:post_id)
.order('min(post_actions.created_at) DESC')
.pluck(:post_id).uniq
post_ids = actions.limit(per_page)
.offset(offset)
.group(:post_id)
.order('min(post_actions.created_at) DESC')
.pluck(:post_id)
.uniq
return nil if post_ids.blank?
actions = actions
.order('post_actions.created_at DESC')
.includes({:related_post => :topic})
posts = SqlBuilder.new("SELECT p.id, t.title, p.cooked, p.user_id,
p.topic_id, p.post_number, p.hidden, t.visible topic_visible,
p.deleted_at, t.deleted_at topic_deleted_at
FROM posts p
JOIN topics t ON t.id = p.topic_id
WHERE p.id in (:post_ids)").map_exec(OpenStruct, post_ids: post_ids)
posts = SqlBuilder.new("
SELECT p.id,
p.cooked,
p.user_id,
p.topic_id,
p.post_number,
p.hidden,
p.deleted_at
FROM posts p
WHERE p.id in (:post_ids)").map_exec(OpenStruct, post_ids: post_ids)
post_lookup = {}
users = Set.new
user_ids = Set.new
topic_ids = Set.new
posts.each do |p|
users << p.user_id
user_ids << p.user_id
topic_ids << p.topic_id
p.excerpt = Post.excerpt(p.cooked)
p.topic_slug = Slug.for(p.title)
p.delete_field(:cooked)
post_lookup[p.id] = p
end
# maintain order
posts = post_ids.map{|id| post_lookup[id]}
post_actions = actions.where(:post_id => post_ids)
post_actions = actions.order('post_actions.created_at DESC')
.includes(related_post: { topic: { posts: :user }})
.where(post_id: post_ids)
post_actions.each do |pa|
post = post_lookup[pa.post_id]
post.post_actions ||= []
action = pa.attributes
# TODO: add serializer so we can skip this
action = {
id: pa.id,
post_id: pa.post_id,
user_id: pa.user_id,
post_action_type_id: pa.post_action_type_id,
created_at: pa.created_at,
disposed_by_id: pa.disposed_by_id,
disposed_at: pa.disposed_at,
disposition: pa.disposition,
related_post_id: pa.related_post_id,
targets_topic: pa.targets_topic,
staff_took_action: pa.staff_took_action
}
action[:name_key] = PostActionType.types.key(pa.post_action_type_id)
if (pa.related_post && pa.related_post.topic)
action.merge!(topic_id: pa.related_post.topic_id,
slug: pa.related_post.topic.slug,
permalink: pa.related_post.topic.url)
if pa.related_post && pa.related_post.topic
conversation = {}
related_topic = pa.related_post.topic
if response = related_topic.posts[0]
conversation[:response] = {
excerpt: excerpt(response.cooked),
user_id: response.user_id
}
user_ids << response.user_id
if reply = related_topic.posts[1]
conversation[:reply] = {
excerpt: excerpt(reply.cooked),
user_id: reply.user_id
}
user_ids << reply.user_id
conversation[:has_more] = related_topic.posts_count > 2
end
end
action.merge!(permalink: related_topic.relative_url, conversation: conversation)
end
post.post_actions << action
users << pa.user_id
users << pa.deleted_by_id if pa.deleted_by_id
user_ids << pa.user_id
user_ids << pa.disposed_by_id if pa.disposed_by_id
end
# TODO add serializer so we can skip this
# maintain order
posts = post_ids.map { |id| post_lookup[id] }
# TODO: add serializer so we can skip this
posts.map!(&:marshal_dump)
[posts, User.where(id: users.to_a).to_a]
[
posts,
Topic.with_deleted.where(id: topic_ids.to_a).to_a,
User.includes(:user_stat).where(id: user_ids.to_a).to_a
]
end
protected
def self.flagged_post_ids(filter, offset, limit)
<<SQL
def self.flagged_post_actions(filter)
post_actions = PostAction.flags
.joins("INNER JOIN posts ON posts.id = post_actions.post_id")
.joins("INNER JOIN topics ON topics.id = posts.topic_id")
SELECT p.id from posts p
JOIN topics t ON t.id = p.topic_id
WHERE p.id IN (
SELECT post_id from post_actions
WHERE
)
/*offset*/
/*limit*/
if filter == "old"
post_actions.with_deleted
.where("post_actions.deleted_at IS NOT NULL OR
post_actions.defered_at IS NOT NULL OR
post_actions.agreed_at IS NOT NULL")
else
post_actions.active
.where("posts.deleted_at" => nil)
.where("topics.deleted_at" => nil)
end
SQL
end
def self.flagged_post_actions(filter)
post_actions = PostAction
.where(post_action_type_id: PostActionType.notify_flag_type_ids)
.joins(:post => :topic)
if filter == 'old'
post_actions
.with_deleted
.where('post_actions.deleted_at IS NOT NULL OR
defer = true OR
topics.deleted_at IS NOT NULL OR
posts.deleted_at IS NOT NULL')
else
post_actions
.where('defer IS NULL OR
defer = false')
.where('posts.deleted_at IS NULL AND
topics.deleted_at IS NULL')
end
end
private
def self.excerpt(cooked)
excerpt = Post.excerpt(cooked, 200)
# remove the first link if it's the first node
fragment = Nokogiri::HTML.fragment(excerpt)
if fragment.children.first == fragment.css("a:first").first
fragment.children.first.remove
end
fragment.to_html.strip
end
end

View File

@ -29,7 +29,7 @@ module PostGuardian
end
end
def can_clear_flags?(post)
def can_defer_flags?(post)
is_staff? && post
end
@ -54,7 +54,11 @@ module PostGuardian
end
def can_delete_all_posts?(user)
is_staff? && user && !user.admin? && (user.first_post.nil? || user.first_post.created_at >= SiteSetting.delete_user_max_post_age.days.ago) && user.post_count <= SiteSetting.delete_all_posts_max.to_i
is_staff? &&
user &&
!user.admin? &&
(user.first_post_created_at.nil? || user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago) &&
user.post_count <= SiteSetting.delete_all_posts_max.to_i
end
# Creating Method

View File

@ -35,12 +35,11 @@ module UserGuardian
end
def can_delete_user?(user)
return false if user.nil?
return false if user.admin?
return false if user.nil? || user.admin?
if is_me?(user)
user.post_count <= 1
else
is_staff? && (user.first_post.nil? || user.first_post.created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago)
is_staff? && (user.first_post_created_at.nil? || user.first_post_created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago)
end
end

View File

@ -61,7 +61,6 @@ class PostCreator
save_post
extract_links
store_unique_post_key
consider_clearing_flags
track_topic
update_topic_stats
update_user_counts
@ -147,21 +146,6 @@ class PostCreator
end
end
def clear_possible_flags(topic)
# at this point we know the topic is a PM and has been replied to ... check if we need to clear any flags
#
first_post = Post.select(:id).where(topic_id: topic.id).find_by("post_number = 1")
post_action = nil
if first_post
post_action = PostAction.find_by(related_post_id: first_post.id, deleted_at: nil, post_action_type_id: PostActionType.types[:notify_moderators])
end
if post_action
post_action.remove_act!(@user)
end
end
private
def setup_topic
@ -233,20 +217,23 @@ class PostCreator
@post.store_unique_post_key
end
def consider_clearing_flags
return if @opts[:import_mode]
return unless @topic.private_message? && @post.post_number > 1 && @topic.user_id != @post.user_id
clear_possible_flags(@topic)
end
def update_user_counts
@user.create_user_stat if @user.user_stat.nil?
if @user.user_stat.first_post_created_at.nil?
@user.user_stat.first_post_created_at = @post.created_at
end
@user.user_stat.post_count += 1
@user.user_stat.topic_count += 1 if @post.post_number == 1
# We don't count replies to your own topics
if !@opts[:import_mode] && @user.id != @topic.user_id
@user.user_stat.update_topic_reply_count
@user.user_stat.save!
end
@user.user_stat.save!
@user.last_posted_at = @post.created_at
@user.save!
end

View File

@ -62,7 +62,8 @@ class PostDestroyer
feature_users_in_the_topic
Topic.reset_highest(@post.topic_id)
end
trash_post_actions
trash_public_post_actions
agree_with_flags
trash_user_actions
@post.update_flagged_posts_count
remove_associated_replies
@ -130,15 +131,18 @@ class PostDestroyer
Jobs.enqueue(:feature_topic_users, topic_id: @post.topic_id, except_post_id: @post.id)
end
def trash_post_actions
@post.post_actions.each do |pa|
pa.trash!(@user)
end
def trash_public_post_actions
public_post_actions = PostAction.publics.where(post_id: @post.id)
public_post_actions.each { |pa| pa.trash!(@user) }
f = PostActionType.types.map{|k,v| ["#{k}_count", 0]}
f = PostActionType.public_types.map { |k,v| ["#{k}_count", 0] }
Post.with_deleted.where(id: @post.id).update_all(Hash[*f.flatten])
end
def agree_with_flags
PostAction.agree_flags!(@post, @user, delete_post: true)
end
def trash_user_actions
UserAction.where(target_post_id: @post.id).each do |ua|
row = {

View File

@ -29,9 +29,7 @@ class PostJobsEnqueuer
end
def after_post_create
if @post.post_number > 1
TopicTrackingState.publish_unread(@post)
end
TopicTrackingState.publish_unread(@post) if @post.post_number > 1
Jobs.enqueue_in(
SiteSetting.email_time_window_mins.minutes,

View File

@ -105,7 +105,7 @@ class PostRevisor
@post.hidden_at = nil
@post.topic.update_attributes(visible: true)
PostAction.clear_flags!(@post, -1)
PostAction.clear_flags!(@post, Discourse.system_user)
end
@post.extract_quoted_post_numbers

View File

@ -23,17 +23,19 @@ describe FlagQuery do
PostAction.act(codinghorror, post2, PostActionType.types[:spam])
PostAction.act(user2, post2, PostActionType.types[:spam])
posts, users = FlagQuery.flagged_posts_report(admin, "")
posts, topics, users = FlagQuery.flagged_posts_report(admin, "")
posts.count.should == 2
first = posts.first
users.count.should == 5
first[:post_actions].count.should == 2
topics.count.should == 2
second = posts[1]
second[:post_actions].count.should == 3
second[:post_actions].first[:permalink].should == mod_message.related_post.topic.url
second[:post_actions].first[:permalink].should == mod_message.related_post.topic.relative_url
posts, users = FlagQuery.flagged_posts_report(admin, "", 1)
posts.count.should == 1

View File

@ -81,25 +81,25 @@ describe Guardian do
end
describe "can_clear_flags" do
describe "can_defer_flags" do
let(:post) { Fabricate(:post) }
let(:user) { post.user }
let(:moderator) { Fabricate(:moderator) }
it "returns false when the user is nil" do
Guardian.new(nil).can_clear_flags?(post).should be_false
Guardian.new(nil).can_defer_flags?(post).should be_false
end
it "returns false when the post is nil" do
Guardian.new(moderator).can_clear_flags?(nil).should be_false
Guardian.new(moderator).can_defer_flags?(nil).should be_false
end
it "returns false when the user is not a moderator" do
Guardian.new(user).can_clear_flags?(post).should be_false
Guardian.new(user).can_defer_flags?(post).should be_false
end
it "returns true when the user is a moderator" do
Guardian.new(moderator).can_clear_flags?(post).should be_true
Guardian.new(moderator).can_defer_flags?(post).should be_true
end
end
@ -1350,7 +1350,7 @@ describe Guardian do
end
context "delete myself" do
let(:myself) { Fabricate.build(:user, created_at: 6.months.ago) }
let(:myself) { Fabricate(:user, created_at: 6.months.ago) }
subject { Guardian.new(myself).can_delete_user?(myself) }
it "is true to delete myself and I have never made a post" do
@ -1375,7 +1375,7 @@ describe Guardian do
it "is true if user is not an admin and first post is not too old" do
user = Fabricate.build(:user, created_at: 100.days.ago)
user.stubs(:first_post).returns(Fabricate.build(:post, created_at: 9.days.ago))
user.stubs(:first_post_created_at).returns(9.days.ago)
SiteSetting.stubs(:delete_user_max_post_age).returns(10)
Guardian.new(actor).can_delete_user?(user).should == true
end
@ -1386,7 +1386,7 @@ describe Guardian do
it "is false if user's first post is too old" do
user = Fabricate.build(:user, created_at: 100.days.ago)
user.stubs(:first_post).returns(Fabricate.build(:post, created_at: 11.days.ago))
user.stubs(:first_post_created_at).returns(11.days.ago)
SiteSetting.stubs(:delete_user_max_post_age).returns(10)
Guardian.new(actor).can_delete_user?(user).should == false
end
@ -1419,19 +1419,19 @@ describe Guardian do
shared_examples "can_delete_all_posts examples" do
it "is true if user has no posts" do
SiteSetting.stubs(:delete_user_max_post_age).returns(10)
Guardian.new(actor).can_delete_all_posts?(Fabricate.build(:user, created_at: 100.days.ago)).should be_true
Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago)).should be_true
end
it "is true if user's first post is newer than delete_user_max_post_age days old" do
user = Fabricate.build(:user, created_at: 100.days.ago)
user.stubs(:first_post).returns(Fabricate.build(:post, created_at: 9.days.ago))
user = Fabricate(:user, created_at: 100.days.ago)
user.stubs(:first_post_created_at).returns(9.days.ago)
SiteSetting.stubs(:delete_user_max_post_age).returns(10)
Guardian.new(actor).can_delete_all_posts?(user).should be_true
end
it "is false if user's first post is older than delete_user_max_post_age days old" do
user = Fabricate.build(:user, created_at: 100.days.ago)
user.stubs(:first_post).returns(Fabricate.build(:post, created_at: 11.days.ago))
user = Fabricate(:user, created_at: 100.days.ago)
user.stubs(:first_post_created_at).returns(11.days.ago)
SiteSetting.stubs(:delete_user_max_post_age).returns(10)
Guardian.new(actor).can_delete_all_posts?(user).should be_false
end
@ -1441,14 +1441,14 @@ describe Guardian do
end
it "is true if number of posts is small" do
u = Fabricate.build(:user, created_at: 1.day.ago)
u = Fabricate(:user, created_at: 1.day.ago)
u.stubs(:post_count).returns(1)
SiteSetting.stubs(:delete_all_posts_max).returns(10)
Guardian.new(actor).can_delete_all_posts?(u).should be_true
end
it "is false if number of posts is not small" do
u = Fabricate.build(:user, created_at: 1.day.ago)
u = Fabricate(:user, created_at: 1.day.ago)
u.stubs(:post_count).returns(11)
SiteSetting.stubs(:delete_all_posts_max).returns(10)
Guardian.new(actor).can_delete_all_posts?(u).should be_false
@ -1528,7 +1528,7 @@ describe Guardian do
end
context 'for a new user' do
let(:target_user) { build(:user, created_at: 1.minute.ago) }
let(:target_user) { Fabricate(:user, created_at: 1.minute.ago) }
include_examples "staff can always change usernames"
it "is true for the user to change their own username" do
@ -1541,7 +1541,7 @@ describe Guardian do
SiteSetting.stubs(:username_change_period).returns(3)
end
let(:target_user) { build(:user, created_at: 4.days.ago) }
let(:target_user) { Fabricate(:user, created_at: 4.days.ago) }
context 'with no posts' do
include_examples "staff can always change usernames"

View File

@ -263,28 +263,24 @@ describe PostDestroyer do
end
describe "post actions" do
let(:codinghorror) { Fabricate(:coding_horror) }
let(:bookmark) { PostAction.new(user_id: post.user_id, post_action_type_id: PostActionType.types[:bookmark] , post_id: post.id) }
let(:second_post) { Fabricate(:post, topic_id: post.topic_id) }
let!(:bookmark) { PostAction.act(moderator, second_post, PostActionType.types[:bookmark]) }
let!(:flag) { PostAction.act(moderator, second_post, PostActionType.types[:off_topic]) }
it "should reset counts when a post is deleted" do
PostAction.act(codinghorror, second_post, PostActionType.types[:off_topic])
expect { PostDestroyer.new(moderator, second_post).destroy }.to change(PostAction, :flagged_posts_count).by(-1)
end
it "should delete public post actions and agree with flags" do
second_post.expects(:update_flagged_posts_count)
it "should delete the post actions" do
flag = PostAction.act(codinghorror, second_post, PostActionType.types[:off_topic])
PostDestroyer.new(moderator, second_post).destroy
expect(PostAction.find_by(id: flag.id)).to be_nil
expect(PostAction.find_by(id: bookmark.id)).to be_nil
end
it 'should update flag counts on the post' do
PostAction.act(codinghorror, second_post, PostActionType.types[:off_topic])
PostDestroyer.new(moderator, second_post.reload).destroy
PostAction.find_by(id: bookmark.id).should == nil
off_topic = PostAction.find_by(id: flag.id)
off_topic.should_not == nil
off_topic.agreed_at.should_not == nil
second_post.reload
expect(second_post.off_topic_count).to eq(0)
expect(second_post.bookmark_count).to eq(0)
second_post.bookmark_count.should == 0
second_post.off_topic_count.should == 1
end
end

View File

@ -307,17 +307,26 @@ describe Admin::UsersController do
response.should be_forbidden
end
it "returns an error if the user has posts" do
Fabricate(:post, user: @delete_me)
xhr :delete, :destroy, id: @delete_me.id
response.should be_forbidden
end
context "user has post" do
before do
@user = build(:user)
@user.stubs(:post_count).returns(1)
@user.stubs(:first_post_created_at).returns(Time.zone.now)
User.expects(:find_by).with(id: @delete_me.id).returns(@user)
end
it "returns an error" do
xhr :delete, :destroy, id: @delete_me.id
response.should be_forbidden
end
it "doesn't return an error if delete_posts == true" do
UserDestroyer.any_instance.expects(:destroy).with(@user, has_entry('delete_posts' => true)).returns(true)
xhr :delete, :destroy, id: @delete_me.id, delete_posts: true
response.should be_success
end
it "doesn't return an error if the user has posts and delete_posts == true" do
Fabricate(:post, user: @delete_me)
UserDestroyer.any_instance.expects(:destroy).with(@delete_me, has_entry('delete_posts' => true)).returns(true)
xhr :delete, :destroy, id: @delete_me.id, delete_posts: true
response.should be_success
end
it "deletes the user record" do

View File

@ -102,13 +102,13 @@ describe PostActionsController do
end
context 'clear_flags' do
context 'defer_flags' do
let(:flagged_post) { Fabricate(:post, user: Fabricate(:coding_horror)) }
context "not logged in" do
it "should not allow them to clear flags" do
lambda { xhr :post, :clear_flags }.should raise_error(Discourse::NotLoggedIn)
lambda { xhr :post, :defer_flags }.should raise_error(Discourse::NotLoggedIn)
end
end
@ -116,43 +116,38 @@ describe PostActionsController do
let!(:user) { log_in(:moderator) }
it "raises an error without a post_action_type_id" do
-> { xhr :post, :clear_flags, id: flagged_post.id }.should raise_error(ActionController::ParameterMissing)
-> { xhr :post, :defer_flags, id: flagged_post.id }.should raise_error(ActionController::ParameterMissing)
end
it "raises an error when the user doesn't have access" do
Guardian.any_instance.expects(:can_clear_flags?).returns(false)
xhr :post, :clear_flags, id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
Guardian.any_instance.expects(:can_defer_flags?).returns(false)
xhr :post, :defer_flags, id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
response.should be_forbidden
end
context "success" do
before do
Guardian.any_instance.expects(:can_clear_flags?).returns(true)
PostAction.expects(:clear_flags!).with(flagged_post, user.id, PostActionType.types[:spam])
Guardian.any_instance.expects(:can_defer_flags?).returns(true)
PostAction.expects(:defer_flags!).with(flagged_post, user)
end
it "delegates to clear_flags" do
xhr :post, :clear_flags, id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
it "delegates to defer_flags" do
xhr :post, :defer_flags, id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
response.should be_success
end
it "works with a deleted post" do
flagged_post.trash!(user)
xhr :post, :clear_flags, id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
xhr :post, :defer_flags, id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
response.should be_success
end
end
end
end
describe 'users' do
let!(:post) { Fabricate(:post, user: log_in) }
@ -188,6 +183,4 @@ describe PostActionsController do
end
end

View File

@ -12,7 +12,6 @@ describe PostAction do
let(:post) { Fabricate(:post) }
let(:bookmark) { PostAction.new(user_id: post.user_id, post_action_type_id: PostActionType.types[:bookmark] , post_id: post.id) }
describe "messaging" do
it "notify moderators integration test" do
@ -41,13 +40,12 @@ describe PostAction do
# Notification level should be "Watching" for everyone
topic.topic_users(true).map(&:notification_level).uniq.should == [TopicUser.notification_levels[:watching]]
# reply to PM should clear flag
# reply to PM should not clear flag
p = PostCreator.new(mod, topic_id: posts[0].topic_id, raw: "This is my test reply to the user, it should clear flags")
p.create
action.reload
action.deleted_at.should_not be_nil
action.deleted_at.should be_nil
end
describe 'notify_moderators' do
@ -87,7 +85,7 @@ describe PostAction do
PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
PostAction.flagged_posts_count.should == 1
PostAction.clear_flags!(post, -1)
PostAction.clear_flags!(post, Discourse.system_user)
PostAction.flagged_posts_count.should == 0
end
@ -103,7 +101,7 @@ describe PostAction do
PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
post.hidden.should be_false
post.hidden_at.should be_blank
PostAction.defer_flags!(post, admin.id)
PostAction.defer_flags!(post, admin)
PostAction.flagged_posts_count.should == 0
post.reload
post.hidden.should be_false
@ -220,7 +218,7 @@ describe PostAction do
# If staff takes action, it is ranked higher
admin = Fabricate(:admin)
pa = PostAction.act(admin, post, PostActionType.types[:spam], take_action: true)
PostAction.act(admin, post, PostActionType.types[:spam], take_action: true)
PostAction.flag_counts_for(post.id).should == [0, 8]
# If a flag is dismissed
@ -252,7 +250,7 @@ describe PostAction do
post.reload
post.spam_count.should == 1
PostAction.clear_flags!(post, -1)
PostAction.clear_flags!(post, Discourse.system_user)
post.reload
post.spam_count.should == 0

View File

@ -764,10 +764,6 @@ describe Topic do
topic.moderator_posts_count.should == 0
end
it "its user has a topics_count of 1" do
topic.user.created_topic_count.should == 1
end
context 'post' do
let(:post) { Fabricate(:post, topic: topic, user: topic.user) }

View File

@ -81,6 +81,10 @@ describe UserDestroyer do
context "delete_posts is false" do
subject(:destroy) { UserDestroyer.new(@admin).destroy(@user) }
before do
@user.stubs(:post_count).returns(1)
@user.stubs(:first_post_created_at).returns(Time.zone.now)
end
it 'should not delete the user' do
expect { destroy rescue nil }.to_not change { User.count }