From d7c37d9369fabaa750c9f26f94eb9f366546e436 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 12 Sep 2017 13:04:53 -0400 Subject: [PATCH] Add front end service for staff controls --- .../admin/adapters/flagged-post.js.es6 | 4 +- .../modals/admin-agree-flag.js.es6 | 4 +- .../modals/admin-delete-flag.js.es6 | 9 +-- .../admin/mixins/delete-spammer-modal.js.es6 | 26 +++++++ .../admin/models/admin-user.js.es6 | 53 ------------- .../routes/admin-flags-topics-show.js.es6 | 3 +- .../admin/services/admin-tools.js.es6 | 75 +++++++++++++++++++ .../admin/templates/flags-topics-show.hbs | 1 + .../templates/modal/admin-agree-flag.hbs | 4 +- .../templates/modal/admin-delete-flag.hbs | 4 +- .../discourse/components/flagged-posts.js.es6 | 24 +----- .../discourse/controllers/flag.js.es6 | 61 ++++++++------- .../discourse/lib/optional-service.js.es6 | 7 ++ .../javascripts/discourse/models/store.js.es6 | 18 +++-- .../inject-discourse-objects.js.es6 | 2 +- .../discourse/routes/application.js.es6 | 4 - .../javascripts/discourse/routes/topic.js.es6 | 4 +- .../discourse/templates/modal/flag.hbs | 4 +- .../stylesheets/common/admin/flagging.scss | 1 + .../admin/flagged_topics_controller.rb | 11 ++- app/controllers/admin/flags_controller.rb | 63 +++++++++------- app/models/post.rb | 2 +- app/models/topic.rb | 2 +- config/routes.rb | 2 +- lib/flag_query.rb | 13 ++-- spec/components/flag_query_spec.rb | 2 +- .../admin/flagged_topics_controller_spec.rb | 19 +++++ .../acceptance/admin-flags-test.js.es6 | 2 + .../javascripts/helpers/flag-pretender.js.es6 | 12 ++- .../helpers/store-pretender.js.es6 | 35 +++++++-- test/javascripts/models/store-test.js.es6 | 9 ++- 31 files changed, 294 insertions(+), 186 deletions(-) create mode 100644 app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 create mode 100644 app/assets/javascripts/admin/services/admin-tools.js.es6 create mode 100644 app/assets/javascripts/discourse/lib/optional-service.js.es6 create mode 100644 spec/requests/admin/flagged_topics_controller_spec.rb diff --git a/app/assets/javascripts/admin/adapters/flagged-post.js.es6 b/app/assets/javascripts/admin/adapters/flagged-post.js.es6 index ed5237588c6..d75afc719fe 100644 --- a/app/assets/javascripts/admin/adapters/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/adapters/flagged-post.js.es6 @@ -2,7 +2,9 @@ import RestAdapter from 'discourse/adapters/rest'; export default RestAdapter.extend({ pathFor(store, type, findArgs) { - return `/admin/flags/${findArgs.filter}.json?rest_api=true`; + let args = Object.assign({ rest_api: true }, findArgs); + delete args.filter; + return `/admin/flags/${findArgs.filter}.json?${$.param(args)}`; }, afterFindAll(results, helper) { diff --git a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 index 8534d64b4fc..07fbe0763c0 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 @@ -1,8 +1,8 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal'; -export default Ember.Controller.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, { removeAfter: null, - deleteSpammer: null, actions: { agreeDeleteSpammer(user) { diff --git a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 index f972465ae97..e8038edbca1 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 @@ -1,15 +1,10 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal'; -export default Ember.Controller.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, { removeAfter: null, actions: { - deleteSpammer(user) { - return this.removeAfter(user.deleteAsSpammer()).then(() => { - this.send('closeModal'); - }); - }, - deletePostDeferFlag() { let flaggedPost = this.get('model'); this.removeAfter(flaggedPost.deferFlags(true)).then(() => { diff --git a/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 b/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 new file mode 100644 index 00000000000..88eb5c0c563 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 @@ -0,0 +1,26 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Mixin.create({ + adminTools: Ember.inject.service(), + spammerDetails: null, + + onShow() { + let adminTools = this.get('adminTools'); + let spammerDetails = adminTools.spammerDetails(this.get('model.user')); + + this.setProperties({ + spammerDetails, + canDeleteSpammer: spammerDetails.canDelete && this.get('model.flaggedForSpam') + }); + }, + + actions: { + deleteSpammer() { + let spammerDetails = this.get('spammerDetails'); + this.removeAfter(spammerDetails.deleteUser()).then(() => { + this.send('closeModal'); + }); + } + } + +}); diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 18431757280..b31bf125654 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -463,59 +463,6 @@ const AdminUser = Discourse.User.extend({ bootbox.dialog(message, buttons, { "classes": "delete-user-modal" }); }, - deleteAsSpammer() { - return this.checkEmail().then(() => { - - let message = I18n.messageFormat('flagging.delete_confirm_MF', { - "POSTS": this.get('post_count'), - "TOPICS": this.get('topic_count'), - email: this.get('email') || I18n.t("flagging.hidden_email_address"), - ip_address: this.get('ip_address') || I18n.t("flagging.ip_address_missing") - }); - - let userId = this.get('id'); - - return new Ember.RSVP.Promise((resolve, reject) => { - const buttons = [ - { - label: I18n.t("composer.cancel"), - class: "cancel-inline", - link: true - }, - { - label: `${iconHTML('exclamation-triangle')} ` + I18n.t("flagging.yes_delete_spammer"), - class: "btn btn-danger confirm-delete", - callback() { - return ajax(`/admin/users/${userId}.json`, { - type: 'DELETE', - data: { - delete_posts: true, - block_email: true, - block_urls: true, - block_ip: true, - delete_as_spammer: true, - context: window.location.pathname - } - }).then(result => { - if (result.deleted) { - resolve(); - } else { - throw 'failed to delete'; - } - }).catch(() => { - bootbox.alert(I18n.t("admin.user.delete_failed")); - reject(); - }); - } - } - ]; - - bootbox.dialog(message, buttons, {classes: "flagging-delete-spammer"}); - }); - - }); - }, - loadDetails() { const user = this; diff --git a/app/assets/javascripts/admin/routes/admin-flags-topics-show.js.es6 b/app/assets/javascripts/admin/routes/admin-flags-topics-show.js.es6 index fca10d61511..b75799bc8cb 100644 --- a/app/assets/javascripts/admin/routes/admin-flags-topics-show.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-flags-topics-show.js.es6 @@ -1,5 +1,4 @@ import { loadTopicView } from 'discourse/models/topic'; -import FlaggedPost from 'admin/models/flagged-post'; export default Ember.Route.extend({ model(params) { @@ -8,7 +7,7 @@ export default Ember.Route.extend({ return Ember.RSVP.hash({ topic, - flaggedPosts: FlaggedPost.findAll({ + flaggedPosts: this.store.findAll('flagged-post', { filter: 'active', topic_id: params.id }) diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js.es6 new file mode 100644 index 00000000000..650e078f57d --- /dev/null +++ b/app/assets/javascripts/admin/services/admin-tools.js.es6 @@ -0,0 +1,75 @@ +// A service that can act as a bridge between the front end Discourse application +// and the admin application. Use this if you need front end code to access admin +// modules. Inject it optionally, and if it exists go to town! + +import AdminUser from 'admin/models/admin-user'; +import { iconHTML } from 'discourse-common/lib/icon-library'; +import { ajax } from 'discourse/lib/ajax'; + +export default Ember.Service.extend({ + + checkSpammer(userId) { + return AdminUser.find(userId).then(au => this.spammerDetails(au)); + }, + + spammerDetails(adminUser) { + return { + deleteUser: () => this._deleteSpammer(adminUser), + canDelete: adminUser.get('can_be_deleted') && adminUser.get('can_delete_all_posts') + }; + }, + + _deleteSpammer(adminUser) { + return adminUser.checkEmail().then(() => { + + let message = I18n.messageFormat('flagging.delete_confirm_MF', { + "POSTS": adminUser.get('post_count'), + "TOPICS": adminUser.get('topic_count'), + email: adminUser.get('email') || I18n.t("flagging.hidden_email_address"), + ip_address: adminUser.get('ip_address') || I18n.t("flagging.ip_address_missing") + }); + + let userId = adminUser.get('id'); + + return new Ember.RSVP.Promise((resolve, reject) => { + const buttons = [ + { + label: I18n.t("composer.cancel"), + class: "cancel-inline", + link: true + }, + { + label: `${iconHTML('exclamation-triangle')} ` + I18n.t("flagging.yes_delete_spammer"), + class: "btn btn-danger confirm-delete", + callback() { + return ajax(`/admin/users/${userId}.json`, { + type: 'DELETE', + data: { + delete_posts: true, + block_email: true, + block_urls: true, + block_ip: true, + delete_as_spammer: true, + context: window.location.pathname + } + }).then(result => { + if (result.deleted) { + resolve(); + } else { + throw 'failed to delete'; + } + }).catch(() => { + bootbox.alert(I18n.t("admin.user.delete_failed")); + reject(); + }); + } + } + ]; + + bootbox.dialog(message, buttons, {classes: "flagging-delete-spammer"}); + }); + + }); + } + +}); diff --git a/app/assets/javascripts/admin/templates/flags-topics-show.hbs b/app/assets/javascripts/admin/templates/flags-topics-show.hbs index 225b21177d4..04947802203 100644 --- a/app/assets/javascripts/admin/templates/flags-topics-show.hbs +++ b/app/assets/javascripts/admin/templates/flags-topics-show.hbs @@ -10,6 +10,7 @@ {{plugin-outlet name="flagged-topic-details-header" args=(hash topic=topic)}} +
{{flagged-posts flaggedPosts=flaggedPosts diff --git a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs index a69ae42877e..b9d5a88383b 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs @@ -24,10 +24,10 @@ icon="thumbs-o-up" label="admin.flags.agree_flag"}} - {{#if model.canDeleteAsSpammer}} + {{#if canDeleteSpammer}} {{d-button title="admin.flags.delete_spammer_title" - action=(action "agreeDeleteSpammer" model.user) + action="deleteSpammer" class="btn-danger delete-spammer" icon="exclamation-triangle" label="admin.flags.delete_spammer"}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs index d27c10d1127..571f332dbf0 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs @@ -13,11 +13,11 @@ icon="thumbs-o-up" label="admin.flags.delete_post_agree_flag"}} - {{#if model.canDeleteAsSpammer}} + {{#if canDeleteSpammer}} {{d-button class="btn-danger delete-spammer" title="admin.flags.delete_spammer_title" - action=(action "deleteSpammer" model.user) + action="deleteSpammer" icon="exclamation-triangle" label="admin.flags.delete_spammer"}} {{/if}} diff --git a/app/assets/javascripts/discourse/components/flagged-posts.js.es6 b/app/assets/javascripts/discourse/components/flagged-posts.js.es6 index 4d520910bf4..67763d54e4f 100644 --- a/app/assets/javascripts/discourse/components/flagged-posts.js.es6 +++ b/app/assets/javascripts/discourse/components/flagged-posts.js.es6 @@ -1,5 +1,3 @@ -import FlaggedPost from 'admin/models/flagged-post'; - export default Ember.Component.extend({ canAct: Ember.computed.equal('filter', 'active'), showResolvedBy: Ember.computed.equal('filter', 'old'), @@ -11,28 +9,10 @@ export default Ember.Component.extend({ }, loadMore() { - if (this.get('allLoaded')) { - return; - } - const flaggedPosts = this.get('flaggedPosts'); - - let args = { - filter: this.get('query'), - offset: flaggedPosts.length+1 - }; - - let topic = this.get('topic'); - if (topic) { - args.topic_id = topic.id; + if (flaggedPosts.get('canLoadMore')) { + flaggedPosts.loadMore(); } - - return FlaggedPost.findAll(args).then(data => { - if (data.length === 0) { - this.set('allLoaded', true); - } - flaggedPosts.addObjects(data); - }); } } }); diff --git a/app/assets/javascripts/discourse/controllers/flag.js.es6 b/app/assets/javascripts/discourse/controllers/flag.js.es6 index 84377f8892f..b5f823500a3 100644 --- a/app/assets/javascripts/discourse/controllers/flag.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag.js.es6 @@ -3,17 +3,35 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ActionSummary from 'discourse/models/action-summary'; import { MAX_MESSAGE_LENGTH } from 'discourse/models/post-action-type'; import computed from 'ember-addons/ember-computed-decorators'; +import optionalService from 'discourse/lib/optional-service'; export default Ember.Controller.extend(ModalFunctionality, { + adminTools: optionalService(), userDetails: null, selected: null, flagTopic: null, message: null, isWarning: false, topicActionByName: null, + spammerDetails: null, onShow() { - this.set('selected', null); + this.setProperties({ + selected: null, + spammerDetails: null + }); + + let adminTools = this.get('adminTools'); + if (adminTools) { + adminTools.checkSpammer(this.get('model.user_id')).then(result => { + this.set('spammerDetails', result); + }); + } + }, + + @computed('spammerDetails.canDelete', 'selected.name_key') + showDeleteSpammer(canDeleteSpammer, nameKey) { + return canDeleteSpammer && nameKey === 'spam'; }, @computed('flagTopic') @@ -74,13 +92,10 @@ export default Ember.Controller.extend(ModalFunctionality, { submitDisabled: Em.computed.not('submitEnabled'), // Staff accounts can "take action" - canTakeAction: function() { - if (this.get("flagTopic")) return false; - - // We can only take actions on non-custom flags - if (this.get('selected.is_custom_flag')) return false; - return Discourse.User.currentProp('staff'); - }.property('selected.is_custom_flag'), + @computed('flagTopic', 'selected.is_custom_flag') + canTakeAction(flagTopic, isCustomFlag) { + return !flagTopic && !isCustomFlag && this.currentUser.get('staff'); + }, submitText: function(){ if (this.get('selected.is_custom_flag')) { @@ -91,6 +106,13 @@ export default Ember.Controller.extend(ModalFunctionality, { }.property('selected.is_custom_flag'), actions: { + deleteSpammer() { + let details = this.get('spammerDetails'); + if (details) { + details.deleteUser().then(() => window.location.reload()); + } + }, + takeAction() { this.send('createFlag', {takeAction: true}); this.set('model.hidden', true); @@ -136,32 +158,9 @@ export default Ember.Controller.extend(ModalFunctionality, { }, }, - canDeleteSpammer: function() { - if (this.get("flagTopic")) return false; - - if (this.currentUser.get('staff') && this.get('selected.name_key') === 'spam') { - return this.get('userDetails.can_be_deleted') && - this.get('userDetails.can_delete_all_posts'); - } else { - return false; - } - }.property('selected.name_key', 'userDetails.can_be_deleted', 'userDetails.can_delete_all_posts'), - @computed('flagTopic', 'selected.name_key') canSendWarning(flagTopic, nameKey) { return !flagTopic && this.currentUser.get('staff') && nameKey === 'notify_user'; - }, - - usernameChanged: function() { - this.set('userDetails', null); - this.fetchUserDetails(); - }.observes('model.username'), - - fetchUserDetails() { - if (Discourse.User.currentProp('staff') && this.get('model.username')) { - const AdminUser = requirejs('admin/models/admin-user').default; - AdminUser.find(this.get('model.user_id')).then(user => this.set('userDetails', user)); - } } }); diff --git a/app/assets/javascripts/discourse/lib/optional-service.js.es6 b/app/assets/javascripts/discourse/lib/optional-service.js.es6 new file mode 100644 index 00000000000..e4053ccc7ef --- /dev/null +++ b/app/assets/javascripts/discourse/lib/optional-service.js.es6 @@ -0,0 +1,7 @@ +const { computed, getOwner, String: { dasherize } } = Ember; + +export default function(name) { + return computed(function(defaultName) { + return getOwner(this).lookup(`service:${name || dasherize(defaultName)}`); + }); +}; diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index e5138109b69..515c135ba8d 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -145,10 +145,12 @@ export default Ember.Object.extend({ const self = this; return ajax(url).then(function(result) { - const typeName = Ember.String.underscore(self.pluralize(type)), - totalRows = result["total_rows_" + typeName] || result.get('totalRows'), - loadMoreUrl = result["load_more_" + typeName], - content = result[typeName].map(obj => self._hydrate(type, obj, result)); + let typeName = Ember.String.underscore(self.pluralize(type)); + + let pageTarget = result.meta || result; + let totalRows = pageTarget["total_rows_" + typeName] || resultSet.get('totalRows'); + let loadMoreUrl = pageTarget["load_more_" + typeName]; + let content = result[typeName].map(obj => self._hydrate(type, obj, result)); resultSet.setProperties({ totalRows, loadMoreUrl }); resultSet.get('content').pushObjects(content); @@ -192,12 +194,14 @@ export default Ember.Object.extend({ const typeName = Ember.String.underscore(this.pluralize(type)); const content = result[typeName].map(obj => this._hydrate(type, obj, result)); + let pageTarget = result.meta || result; + const createArgs = { content, findArgs, - totalRows: result["total_rows_" + typeName] || content.length, - loadMoreUrl: result["load_more_" + typeName], - refreshUrl: result['refresh_' + typeName], + totalRows: pageTarget["total_rows_" + typeName] || content.length, + loadMoreUrl: pageTarget["load_more_" + typeName], + refreshUrl: pageTarget['refresh_' + typeName], store: this, __type: type }; diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 index 51849ef4025..34181e46b41 100644 --- a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 @@ -30,7 +30,7 @@ export default { DiscourseURL.appEvents = appEvents; app.register('store:main', Store); - inject(app, 'store', 'route', 'controller'); + inject(app, 'store', 'route', 'controller', 'service'); const messageBus = window.MessageBus; app.register('message-bus:main', messageBus, { instantiate: false }); diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index c53dd857129..a035ef6c70c 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -138,10 +138,6 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { }); }, - deleteSpammer(user) { - user.deleteAsSpammer.then(() => window.location.reload()); - }, - checkEmail(user) { user.checkEmail(); }, diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 5f920b58fd6..7fc7fd098fd 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -41,13 +41,13 @@ const TopicRoute = Discourse.Route.extend({ showFlags(model) { let controller = showModal('flag', { model }); - controller.setProperties({ selected: null, flagTopic: false }); + controller.setProperties({ flagTopic: false }); }, showFlagTopic() { const model = this.modelFor('topic'); let controller = showModal('flag', { model }); - controller.setProperties({ selected: null, flagTopic: true }); + controller.setProperties({ flagTopic: true }); }, showTopicStatusUpdate() { diff --git a/app/assets/javascripts/discourse/templates/modal/flag.hbs b/app/assets/javascripts/discourse/templates/modal/flag.hbs index 26463f09efb..4123a371bb7 100644 --- a/app/assets/javascripts/discourse/templates/modal/flag.hbs +++ b/app/assets/javascripts/discourse/templates/modal/flag.hbs @@ -39,10 +39,10 @@ label="flagging.take_action"}} {{/if}} - {{#if canDeleteSpammer}} + {{#if showDeleteSpammer}} {{d-button class="btn-danger" - action=(route-action "deleteSpammer" userDetails) + action="deleteSpammer" disabled=submitDisabled icon="exclamation-triangle" label="flagging.delete_spammer"}} diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index b5f290c5962..03393450846 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -152,6 +152,7 @@ .flagged-topic-details { display: flex; justify-content: space-between; + margin-bottom: 2em; } .delete-flag-modal, .agree-flag-modal { diff --git a/app/controllers/admin/flagged_topics_controller.rb b/app/controllers/admin/flagged_topics_controller.rb index d84d89680c3..82e7b68b27f 100644 --- a/app/controllers/admin/flagged_topics_controller.rb +++ b/app/controllers/admin/flagged_topics_controller.rb @@ -5,10 +5,13 @@ class Admin::FlaggedTopicsController < Admin::AdminController def index result = FlagQuery.flagged_topics - render_json_dump({ - flagged_topics: serialize_data(result[:flagged_topics], FlaggedTopicSummarySerializer), - users: serialize_data(result[:users], BasicUserSerializer), - }, rest_serializer: true) + render_json_dump( + { + flagged_topics: serialize_data(result[:flagged_topics], FlaggedTopicSummarySerializer), + users: serialize_data(result[:users], BasicUserSerializer), + }, + rest_serializer: true + ) end end diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb index 16d4b03fb69..a14809476ff 100644 --- a/app/controllers/admin/flags_controller.rb +++ b/app/controllers/admin/flags_controller.rb @@ -10,40 +10,51 @@ class Admin::FlagsController < Admin::AdminController # we may get out of sync, fix it here PostAction.update_flagged_posts_count - posts, topics, users, post_actions = FlagQuery.flagged_posts_report( + offset = params[:offset].to_i + per_page = Admin::FlagsController.flags_per_page + + posts, topics, users, post_actions, total_rows = FlagQuery.flagged_posts_report( current_user, filter: params[:filter], - offset: params[:offset].to_i, + offset: offset, topic_id: params[:topic_id], - per_page: Admin::FlagsController.flags_per_page, + per_page: per_page, rest_api: params[:rest_api].present? ) - if posts.blank? - render json: { posts: [], topics: [], users: [] } - else - if params[:rest_api] - render_json_dump( - { - flagged_posts: posts, - topics: serialize_data(topics, FlaggedTopicSerializer), - users: serialize_data(users, FlaggedUserSerializer), - post_actions: post_actions - }, - rest_serializer: true, - meta: { - types: { - disposed_by: 'user' - } - } - ) - else - render_json_dump( - posts: posts, - topics: serialize_data(topics, FlaggedTopicSerializer), - users: serialize_data(users, FlaggedUserSerializer) + if params[:rest_api] + meta = { + types: { + disposed_by: 'user' + } + } + + if (total_rows || 0) > (offset + per_page) + meta[:total_rows_flagged_posts] = total_rows + meta[:load_more_flagged_posts] = admin_flags_filtered_path( + filter: params[:filter], + offset: offset + per_page, + rest_api: params[:rest_api], + topic_id: params[:topic_id] ) end + + render_json_dump( + { + flagged_posts: posts, + topics: serialize_data(topics, FlaggedTopicSerializer), + users: serialize_data(users, FlaggedUserSerializer), + post_actions: post_actions + }, + rest_serializer: true, + meta: meta + ) + else + render_json_dump( + posts: posts, + topics: serialize_data(topics, FlaggedTopicSerializer), + users: serialize_data(users, FlaggedUserSerializer) + ) end end diff --git a/app/models/post.rb b/app/models/post.rb index d47bf02b456..853204830d0 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -317,7 +317,7 @@ class Post < ActiveRecord::Base end def archetype - topic.archetype + topic&.archetype end def self.regular_order diff --git a/app/models/topic.rb b/app/models/topic.rb index 6730091581b..22547b743f7 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -272,7 +272,7 @@ class Topic < ActiveRecord::Base end def has_flags? - FlagQuery.flagged_post_actions("active") + FlagQuery.flagged_post_actions(filter: "active") .where("topics.id" => id) .exists? end diff --git a/config/routes.rb b/config/routes.rb index 33389b5e107..2ebd987a426 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -187,7 +187,7 @@ Discourse::Application.routes.draw do put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new get "flags" => "flags#index" - get "flags/:filter" => "flags#index" + get "flags/:filter" => "flags#index", as: 'flags_filtered' get "flags/topics/:topic_id" => "flags#index" post "flags/agree/:id" => "flags#agree" post "flags/disagree/:id" => "flags#disagree" diff --git a/lib/flag_query.rb b/lib/flag_query.rb index dbbdd7964ee..91deb31dc59 100644 --- a/lib/flag_query.rb +++ b/lib/flag_query.rb @@ -4,10 +4,8 @@ module FlagQuery def self.flagged_posts_report(current_user, opts = nil) opts ||= {} - filter = opts[:filter] || 'active' offset = opts[:offset] || 0 per_page = opts[:per_page] || 25 - topic_id = opts[:topic_id] actions = flagged_post_actions(opts) @@ -21,6 +19,8 @@ module FlagQuery ) end + total_rows = actions.count + post_ids = actions.limit(per_page) .offset(offset) .group(:post_id) @@ -28,8 +28,6 @@ module FlagQuery .pluck(:post_id) .uniq - return nil if post_ids.blank? - posts = SqlBuilder.new(" SELECT p.id, p.cooked, @@ -129,11 +127,14 @@ module FlagQuery posts, Topic.with_deleted.where(id: topic_ids.to_a).to_a, User.includes(:user_stat).where(id: user_ids.to_a).to_a, - all_post_actions + all_post_actions, + total_rows ] end - def self.flagged_post_actions(opts) + def self.flagged_post_actions(opts = nil) + opts ||= {} + 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") diff --git a/spec/components/flag_query_spec.rb b/spec/components/flag_query_spec.rb index 5a63539ea69..7e39c61feff 100644 --- a/spec/components/flag_query_spec.rb +++ b/spec/components/flag_query_spec.rb @@ -57,7 +57,7 @@ describe FlagQuery do posts = FlagQuery.flagged_posts_report(admin, topic_id: post.topic_id) expect(posts).to be_present posts = FlagQuery.flagged_posts_report(admin, topic_id: -1) - expect(posts).to be_blank + expect(posts[0]).to be_blank # chuck post in category a mod can not see and make sure its missing category = Fabricate(:category) diff --git a/spec/requests/admin/flagged_topics_controller_spec.rb b/spec/requests/admin/flagged_topics_controller_spec.rb new file mode 100644 index 00000000000..594caff1ed7 --- /dev/null +++ b/spec/requests/admin/flagged_topics_controller_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +RSpec.describe Admin::FlaggedTopicsController do + let(:admin) { Fabricate(:admin) } + let!(:flag) { Fabricate(:flag) } + + before do + sign_in(admin) + end + + it "returns a list of flagged topics" do + get "/admin/flagged_topics.json" + expect(response).to be_success + + data = ::JSON.parse(response.body) + expect(data['flagged_topics']).to be_present + expect(data['users']).to be_present + end +end diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index aa799b99a6a..e891e568c4e 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -6,6 +6,8 @@ QUnit.test("flagged posts", assert => { andThen(() => { assert.equal(find('.flagged-posts .flagged-post').length, 1); assert.equal(find('.flagged-post .flaggers .flagger').length, 1, 'shows who flagged it'); + assert.equal(find('.flagged-post-response').length, 2); + assert.equal(find('.flagged-post-response:eq(0) img.avatar').length, 1); }); }); diff --git a/test/javascripts/helpers/flag-pretender.js.es6 b/test/javascripts/helpers/flag-pretender.js.es6 index 68ffd308664..f42221453c2 100644 --- a/test/javascripts/helpers/flag-pretender.js.es6 +++ b/test/javascripts/helpers/flag-pretender.js.es6 @@ -49,7 +49,17 @@ export default function(helpers) { id: 1, user_id: eviltrout.id, post_action_type_id: 8, - name_key: 'spam' + name_key: 'spam', + conversation: { + response: { + user_id: eviltrout.id, + excerpt: "hello", + }, + reply: { + user_id: eviltrout.id, + excerpt: "goodbye" + } + } }], "__rest_serializer": "1" }); diff --git a/test/javascripts/helpers/store-pretender.js.es6 b/test/javascripts/helpers/store-pretender.js.es6 index c47dec3ab93..d86f1260e81 100644 --- a/test/javascripts/helpers/store-pretender.js.es6 +++ b/test/javascripts/helpers/store-pretender.js.es6 @@ -28,7 +28,26 @@ export default function(helpers) { }); this.get('/fruits', function() { - return response({ __rest_serializer: "1", fruits, farmers, colors, extras: {hello: 'world'} }); + return response({ + __rest_serializer: "1", + fruits, + farmers, + colors, + extras: {hello: 'world'} + }); + }); + + this.get('/barns/:id', function() { + return response({ + __rest_serializer: "1", + meta: { + types: { + owner: "farmer" + } + }, + barn: { id: 1234, owner_id: farmers[0].id }, + farmers: [farmers[0]], + }); }); this.get('/widgets/:widget_id', function(request) { @@ -65,10 +84,14 @@ export default function(helpers) { if (qp.id) { result = result.filterBy('id', parseInt(qp.id)); } } - return response({ widgets: result, - total_rows_widgets: 4, - load_more_widgets: '/load-more-widgets', - refresh_widgets: '/widgets?refresh=true' }); + return response({ + widgets: result, + meta: { + total_rows_widgets: 4, + load_more_widgets: '/load-more-widgets', + refresh_widgets: '/widgets?refresh=true' + } + }); }); this.get('/load-more-widgets', function() { @@ -76,4 +99,4 @@ export default function(helpers) { }); this.delete('/widgets/:widget_id', success); -}; \ No newline at end of file +}; diff --git a/test/javascripts/models/store-test.js.es6 b/test/javascripts/models/store-test.js.es6 index f532ad6f77d..6499262e8b1 100644 --- a/test/javascripts/models/store-test.js.es6 +++ b/test/javascripts/models/store-test.js.es6 @@ -142,6 +142,13 @@ QUnit.test('find embedded', function(assert) { }); }); +QUnit.test('meta types', function(assert) { + const store = createStore(); + return store.find('barn', 1).then(function(f) { + assert.equal(f.get('owner.name'), 'Old MacDonald', 'it has the embedded farmer'); + }); +}); + QUnit.test('findAll embedded', function(assert) { const store = createStore(); return store.findAll('fruit').then(function(fruits) { @@ -156,4 +163,4 @@ QUnit.test('findAll embedded', function(assert) { assert.equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker'); }); -}); \ No newline at end of file +});