diff --git a/app/assets/javascripts/admin/controllers/admin-flags.js.es6 b/app/assets/javascripts/admin/controllers/admin-flags.js.es6 index 7e4542cd8d5..93cc6d6772d 100644 --- a/app/assets/javascripts/admin/controllers/admin-flags.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-flags.js.es6 @@ -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){ diff --git a/app/assets/javascripts/admin/controllers/admin_delete_flag_controller.js b/app/assets/javascripts/admin/controllers/admin_delete_flag_controller.js new file mode 100644 index 00000000000..89ee5371dd5 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin_delete_flag_controller.js @@ -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(); }); + } + } + +}); diff --git a/app/assets/javascripts/admin/models/flagged_post.js b/app/assets/javascripts/admin/models/flagged_post.js index 8e424baf1c4..98110d0e2c9 100644 --- a/app/assets/javascripts/admin/models/flagged_post.js +++ b/app/assets/javascripts/admin/models/flagged_post.js @@ -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; }); } }); - - diff --git a/app/assets/javascripts/admin/routes/admin_flags_route.js b/app/assets/javascripts/admin/routes/admin_flags_route.js index 8941d7010f9..a2bf110ea60 100644 --- a/app/assets/javascripts/admin/routes/admin_flags_route.js +++ b/app/assets/javascripts/admin/routes/admin_flags_route.js @@ -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'); + } + + } }); diff --git a/app/assets/javascripts/admin/templates/flags.js.handlebars b/app/assets/javascripts/admin/templates/flags.js.handlebars index 5329ec63ebc..52224ed9010 100644 --- a/app/assets/javascripts/admin/templates/flags.js.handlebars +++ b/app/assets/javascripts/admin/templates/flags.js.handlebars @@ -8,10 +8,10 @@
- {{#if model.loading}} + {{#if loading}}
{{i18n loading}}
{{else}} - {{#if model.length}} + {{#if length}} @@ -19,131 +19,141 @@ - - {{#each flaggedPost in content}} - + - + - + - + - - + + + {{#if flaggedPost.topicFlagged}} - - - - + + {{/if}} - {{#each flaggedPost.messages}} - + {{#each flaggedPost.conversations}} + - - - {{/each}} - + - + {{/each}} diff --git a/app/assets/javascripts/admin/templates/modal/admin_delete_flag.js.handlebars b/app/assets/javascripts/admin/templates/modal/admin_delete_flag.js.handlebars new file mode 100644 index 00000000000..e1711062b08 --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin_delete_flag.js.handlebars @@ -0,0 +1,5 @@ + + +{{#if canDeleteAsSpammer}} + +{{/if}} diff --git a/app/assets/javascripts/admin/views/admin_flags_view.js b/app/assets/javascripts/admin/views/admin_flags_view.js index 823dd4aa379..52053a53895 100644 --- a/app/assets/javascripts/admin/views/admin_flags_view.js +++ b/app/assets/javascripts/admin/views/admin_flags_view.js @@ -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); + }); + } + } }); diff --git a/app/assets/javascripts/admin/views/modals/admin_delete_flag_view.js b/app/assets/javascripts/admin/views/modals/admin_delete_flag_view.js new file mode 100644 index 00000000000..204e7b382a1 --- /dev/null +++ b/app/assets/javascripts/admin/views/modals/admin_delete_flag_view.js @@ -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') +}); diff --git a/app/assets/javascripts/discourse/components/discourse-action-history.js.es6 b/app/assets/javascripts/discourse/components/discourse-action-history.js.es6 index 6e089d90935..2451275db93 100644 --- a/app/assets/javascripts/discourse/components/discourse-action-history.js.es6 +++ b/app/assets/javascripts/discourse/components/discourse-action-history.js.es6 @@ -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(""); }); @@ -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; } diff --git a/app/assets/javascripts/discourse/components/topic-status.js.es6 b/app/assets/javascripts/discourse/components/topic-status.js.es6 index fdc8585877c..606471b4ee7 100644 --- a/app/assets/javascripts/discourse/components/topic-status.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-status.js.es6 @@ -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'); diff --git a/app/assets/javascripts/discourse/models/action_summary.js b/app/assets/javascripts/discourse/models/action_summary.js index 47ddddeccd9..f1d4abb53de 100644 --- a/app/assets/javascripts/discourse/models/action_summary.js +++ b/app/assets/javascripts/discourse/models/action_summary.js @@ -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'), diff --git a/app/assets/javascripts/discourse/templates/header.js.handlebars b/app/assets/javascripts/discourse/templates/header.js.handlebars index 825856c9b57..24bc9d0e2fa 100644 --- a/app/assets/javascripts/discourse/templates/header.js.handlebars +++ b/app/assets/javascripts/discourse/templates/header.js.handlebars @@ -13,20 +13,20 @@ {{#if topic.isPrivateMessage}} {{icon envelope}} {{/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}} - {{{topic.fancy_title}}} - {{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}} + {{{topic.fancy_title}}} + {{else}} + {{#if topic.errorLoading}} + {{topic.errorTitle}} + {{else}} + {{i18n topic.loading}} + {{/if}} {{/if}} - {{/if}} diff --git a/app/assets/javascripts/discourse/templates/topic.js.handlebars b/app/assets/javascripts/discourse/templates/topic.js.handlebars index 0fe6f9f5e31..081fd025154 100644 --- a/app/assets/javascripts/discourse/templates/topic.js.handlebars +++ b/app/assets/javascripts/discourse/templates/topic.js.handlebars @@ -17,7 +17,7 @@ {{#if editingTopic}} {{#if isPrivateMessage}} - + {{icon envelope}} {{else}} {{category-chooser valueAttribute="id" value=newCategoryId source=category_id}} {{/if}} diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 676a4681000..161816b1fa5 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -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%; - } } diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb index f7747414007..cf57307258a 100644 --- a/app/controllers/admin/flags_controller.rb +++ b/app/controllers/admin/flags_controller.rb @@ -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 diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b7a4123a3c6..16e2d13fb8d 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -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)) diff --git a/app/controllers/post_actions_controller.rb b/app/controllers/post_actions_controller.rb index 9a04cf0aa72..7a2fbbd0192 100644 --- a/app/controllers/post_actions_controller.rb +++ b/app/controllers/post_actions_controller.rb @@ -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 diff --git a/app/models/post_action.rb b/app/models/post_action.rb index a5b5e308c7a..00e4002c7c2 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -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 # diff --git a/app/models/post_action_type.rb b/app/models/post_action_type.rb index 90b7f330ffc..e46928ff105 100644 --- a/app/models/post_action_type.rb +++ b/app/models/post_action_type.rb @@ -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 diff --git a/app/models/report.rb b/app/models/report.rb index 3caec73f06b..bf6b04b26f9 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -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 diff --git a/app/models/topic.rb b/app/models/topic.rb index 71fde80df1c..d9821f83b04 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -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? diff --git a/app/models/user.rb b/app/models/user.rb index 6762bcfdf2b..bbeb787adde 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/serializers/flagged_topic_serializer.rb b/app/serializers/flagged_topic_serializer.rb new file mode 100644 index 00000000000..2e5e08b0866 --- /dev/null +++ b/app/serializers/flagged_topic_serializer.rb @@ -0,0 +1,10 @@ +class FlaggedTopicSerializer < ActiveModel::Serializer + attributes :id, + :title, + :slug, + :archived, + :closed, + :visible, + :archetype, + :relative_url +end diff --git a/app/serializers/flagged_user_serializer.rb b/app/serializers/flagged_user_serializer.rb new file mode 100644 index 00000000000..899602b9feb --- /dev/null +++ b/app/serializers/flagged_user_serializer.rb @@ -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 diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 921341cf4cb..3eae60a16e3 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -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) diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 34998320690..23313c29daa 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0d351cba81e..bc696866cf5 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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 topic has been flagged." visit_topic: "Visit the topic to take action" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d8bebc18818..43e5c342a0a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index f87dcf0d02d..771cf52570b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20140721161249_add_agreed_at_and_agreed_by_id_to_post_action.rb b/db/migrate/20140721161249_add_agreed_at_and_agreed_by_id_to_post_action.rb new file mode 100644 index 00000000000..c676a0faf5c --- /dev/null +++ b/db/migrate/20140721161249_add_agreed_at_and_agreed_by_id_to_post_action.rb @@ -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 diff --git a/db/migrate/20140721162307_rename_defer_columns_on_post_action.rb b/db/migrate/20140721162307_rename_defer_columns_on_post_action.rb new file mode 100644 index 00000000000..7a02bc139a7 --- /dev/null +++ b/db/migrate/20140721162307_rename_defer_columns_on_post_action.rb @@ -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 diff --git a/db/migrate/20140725172830_remove_message_from_post_action.rb b/db/migrate/20140725172830_remove_message_from_post_action.rb new file mode 100644 index 00000000000..778d99a7f88 --- /dev/null +++ b/db/migrate/20140725172830_remove_message_from_post_action.rb @@ -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 diff --git a/db/migrate/20140728120708_fix_index_on_post_action.rb b/db/migrate/20140728120708_fix_index_on_post_action.rb new file mode 100644 index 00000000000..23be8acecdd --- /dev/null +++ b/db/migrate/20140728120708_fix_index_on_post_action.rb @@ -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 diff --git a/db/migrate/20140728144308_add_first_post_created_at_to_user_stat.rb b/db/migrate/20140728144308_add_first_post_created_at_to_user_stat.rb new file mode 100644 index 00000000000..34dfca71cb5 --- /dev/null +++ b/db/migrate/20140728144308_add_first_post_created_at_to_user_stat.rb @@ -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 diff --git a/db/migrate/20140728152804_add_post_and_topic_counts_to_user_stat.rb b/db/migrate/20140728152804_add_post_and_topic_counts_to_user_stat.rb new file mode 100644 index 00000000000..3152191d16b --- /dev/null +++ b/db/migrate/20140728152804_add_post_and_topic_counts_to_user_stat.rb @@ -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 diff --git a/lib/flag_query.rb b/lib/flag_query.rb index 41324f9bab0..7178bba2c90 100644 --- a/lib/flag_query.rb +++ b/lib/flag_query.rb @@ -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) - < 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 diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 17dcaf83d8e..5647f71ff89 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -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 diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index 3e2bafd3be5..23c964e1236 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -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 diff --git a/lib/post_creator.rb b/lib/post_creator.rb index ecf30b7ca91..957d5bf0cd5 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -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 diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 3f94b146e25..7ade50a8f23 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -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 = { diff --git a/lib/post_jobs_enqueuer.rb b/lib/post_jobs_enqueuer.rb index 219ea863fc3..fcb2f13e835 100644 --- a/lib/post_jobs_enqueuer.rb +++ b/lib/post_jobs_enqueuer.rb @@ -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, diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index b95681aebf4..cf6d353d42d 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -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 diff --git a/spec/components/flag_query_spec.rb b/spec/components/flag_query_spec.rb index 24f6ef6772c..a6a1ac3f9b7 100644 --- a/spec/components/flag_query_spec.rb +++ b/spec/components/flag_query_spec.rb @@ -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 diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 639dd5b6310..9af5d7f8032 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -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" diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index ab68f35a459..609e0040b81 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -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 diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index ba4dcc65fe8..8831e660bf9 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -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 diff --git a/spec/controllers/post_actions_controller_spec.rb b/spec/controllers/post_actions_controller_spec.rb index aa81cfc2d76..71ba8a6939f 100644 --- a/spec/controllers/post_actions_controller_spec.rb +++ b/spec/controllers/post_actions_controller_spec.rb @@ -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 diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index 587755e05b1..4427be8f419 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -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 diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index ca1b5676d68..4fd096b0381 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -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) } diff --git a/spec/services/user_destroyer_spec.rb b/spec/services/user_destroyer_spec.rb index 1d1d10db452..bf26b55f760 100644 --- a/spec/services/user_destroyer_spec.rb +++ b/spec/services/user_destroyer_spec.rb @@ -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 }
{{i18n admin.flags.flagged_by}} {{#if adminOldFlagsView}}{{i18n admin.flags.resolved_by}}{{/if}}
- {{#if flaggedPost.postAuthorFlagged}} - {{#if flaggedPost.user}} - {{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}} + + {{#if flaggedPost.postAuthorFlagged}} + {{#if flaggedPost.user}} + {{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}} + {{/if}} {{/if}} - {{/if}} - - {{#if flaggedPost.topicHidden}} {{/if}}

{{flaggedPost.title}}

-
- {{#if flaggedPost.postAuthorFlagged}} - {{{flaggedPost.excerpt}}} - {{/if}} -
+

+ {{#if flaggedPost.topic.isPrivateMessage}} + {{icon envelope}} + {{/if}} + {{topic-status topic=flaggedPost.topic}} + {{flaggedPost.topic.title}} +

+ {{#if flaggedPost.postAuthorFlagged}} + {{{flaggedPost.excerpt}}} + {{/if}} +
- - - {{#each flaggedPost.flaggers}} - - - - - - {{/each}} - -
- {{#link-to 'adminUser' this.user}}{{avatar this.user imageSize="small"}} {{/link-to}} - - {{date this.flaggedAt}} - - {{this.flagType}} -
-
+ + + {{#each flaggedPost.flaggers}} + + + + + + {{/each}} + +
+ {{#link-to 'adminUser' user}} + {{avatar user imageSize="small"}} + {{/link-to}} + + {{date flaggedAt}} + + {{flagType}} +
+
- - - {{#each flaggedPost.flaggers}} - - {{#if deletedBy}} - - - - {{/if}} - - {{/each}} - -
- {{#link-to 'adminUser' this.deletedBy}}{{avatar this.deletedBy imageSize="small"}} {{/link-to}} - - {{#if this.tookAction}} - - {{/if}} - - {{date this.deletedAt}} -
-
+ + + {{#each flaggedPost.flaggers}} + + + + + + {{/each}} + +
+ {{#link-to 'adminUser' disposedBy}} + {{avatar disposedBy imageSize="small"}} + {{/link-to}} + + {{date disposedAt}} + + {{disposition}} + {{#if tookAction}} + + {{/if}} +
+
{{{i18n admin.flags.topic_flagged}}}
+
+ {{{i18n admin.flags.topic_flagged}}} +
+
+
- {{#unless bySystemUser}} - {{#link-to 'adminUser' user}}{{avatar user imageSize="small"}}{{/link-to}} - {{message}} - - {{else}} - {{i18n admin.flags.system}}: - {{message}} - {{/unless}} + {{#if response}} +

+ {{#link-to 'adminUser' response.user}}{{avatar response.user imageSize="small"}}{{/link-to}} {{{response.excerpt}}} +

+ {{#if reply}} +

+ {{#link-to 'adminUser' reply.user}}{{avatar reply.user imageSize="small"}}{{/link-to}} {{{reply.excerpt}}} + {{#if hasMore}} + {{i18n admin.flags.more}} + {{/if}} +

+ {{/if}} + + + + {{/if}}
- {{#if adminActiveFlagsView}} - {{#if flaggedPost.topicFlagged}} - {{i18n admin.flags.visit_topic}} - {{/if}} + {{#if adminActiveFlagsView}} + {{#if flaggedPost.topicFlagged}} + {{i18n admin.flags.visit_topic}} + {{/if}} - {{#if flaggedPost.postAuthorFlagged}} - {{#if flaggedPost.postHidden}} - - + {{#if flaggedPost.postAuthorFlagged}} + {{#if flaggedPost.postHidden}} + + {{else}} + + + {{/if}} + + {{else}} - - + {{/if}} - - {{#if flaggedPost.canDeleteAsSpammer}} - - {{/if}} - - - {{else}} - {{/if}} - {{/if}}