From 40eba8cd93465d902112f1925d0f59b0d010abe6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Sep 2017 10:21:07 -0400 Subject: [PATCH] FEATURE: View flags grouped by topic --- .../admin/components/flag-counts.js.es6 | 10 ++ .../javascripts/admin/models/flag-type.js.es6 | 10 ++ .../admin/models/flagged-post.js.es6 | 133 +++++++++--------- .../routes/admin-flags-topics-index.js.es6 | 9 ++ .../routes/admin-flags-topics-show.js.es6 | 18 +++ .../admin/routes/admin-route-map.js.es6 | 3 + .../templates/components/flag-counts.hbs | 2 + .../components/flagged-topic-users.hbs | 5 + .../admin/templates/flags-topics-index.hbs | 42 ++++++ .../admin/templates/flags-topics-show.hbs | 11 ++ .../javascripts/admin/templates/flags.hbs | 5 +- .../discourse-common/resolver.js.es6 | 1 - .../discourse/adapters/rest.js.es6 | 10 +- .../discourse/components/plugin-outlet.js.es6 | 6 + .../stylesheets/common/admin/admin_base.scss | 1 + .../common/admin/flagged_topics.scss | 28 ++++ .../admin/flagged_topics_controller.rb | 14 ++ app/controllers/application_controller.rb | 2 +- app/models/concerns/has_custom_fields.rb | 1 + .../flagged_topic_summary_serializer.rb | 29 ++++ config/locales/client.en.yml | 14 +- config/routes.rb | 3 + lib/flag_query.rb | 40 ++++++ lib/json_error.rb | 7 +- .../acceptance/admin-flags-topics-test.js.es6 | 11 ++ .../helpers/create-pretender.js.es6 | 8 ++ test/javascripts/helpers/create-store.js.es6 | 3 +- 27 files changed, 347 insertions(+), 79 deletions(-) create mode 100644 app/assets/javascripts/admin/components/flag-counts.js.es6 create mode 100644 app/assets/javascripts/admin/models/flag-type.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-flags-topics-index.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-flags-topics-show.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/flag-counts.hbs create mode 100644 app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs create mode 100644 app/assets/javascripts/admin/templates/flags-topics-index.hbs create mode 100644 app/assets/javascripts/admin/templates/flags-topics-show.hbs create mode 100644 app/assets/stylesheets/common/admin/flagged_topics.scss create mode 100644 app/controllers/admin/flagged_topics_controller.rb create mode 100644 app/serializers/flagged_topic_summary_serializer.rb create mode 100644 test/javascripts/acceptance/admin-flags-topics-test.js.es6 diff --git a/app/assets/javascripts/admin/components/flag-counts.js.es6 b/app/assets/javascripts/admin/components/flag-counts.js.es6 new file mode 100644 index 00000000000..040207e6955 --- /dev/null +++ b/app/assets/javascripts/admin/components/flag-counts.js.es6 @@ -0,0 +1,10 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + classNames: ['flag-counts'], + + @computed('details.flag_type_id') + title(id) { + return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 }); + } +}); diff --git a/app/assets/javascripts/admin/models/flag-type.js.es6 b/app/assets/javascripts/admin/models/flag-type.js.es6 new file mode 100644 index 00000000000..54ef7f26c16 --- /dev/null +++ b/app/assets/javascripts/admin/models/flag-type.js.es6 @@ -0,0 +1,10 @@ +import RestModel from 'discourse/models/rest'; +import computed from 'ember-addons/ember-computed-decorators'; + +export default RestModel.extend({ + @computed('id') + name(id) { + return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1}); + } +}); + diff --git a/app/assets/javascripts/admin/models/flagged-post.js.es6 b/app/assets/javascripts/admin/models/flagged-post.js.es6 index 7a627f1569b..c1517836dbd 100644 --- a/app/assets/javascripts/admin/models/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/models/flagged-post.js.es6 @@ -3,37 +3,35 @@ import AdminUser from 'admin/models/admin-user'; import Topic from 'discourse/models/topic'; import Post from 'discourse/models/post'; import { iconHTML } from 'discourse-common/lib/icon-library'; +import computed from 'ember-addons/ember-computed-decorators'; const FlaggedPost = Post.extend({ - summary: function () { + @computed + summary() { 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 }); }) .join(','); - }.property(), + }, - flaggers: function () { - var self = this; - var flaggers = []; - - _.each(this.post_actions, function (postAction) { - flaggers.push({ - user: self.userLookup[postAction.user_id], - topic: self.topicLookup[postAction.topic_id], + @computed + flaggers() { + return this.post_actions.map(postAction => { + return { + user: this.userLookup[postAction.user_id], + topic: this.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, + disposedBy: postAction.disposed_by_id ? this.userLookup[postAction.disposed_by_id] : null, disposedAt: postAction.disposed_at, - dispositionIcon: self.dispositionIcon(postAction.disposition), + dispositionIcon: this.dispositionIcon(postAction.disposition), tookAction: postAction.staff_took_action - }); + } }); + }, - return flaggers; - }.property(), - - dispositionIcon: function (disposition) { + dispositionIcon(disposition) { if (!disposition) { return null; } let icon; let title = 'admin.flags.dispositions.' + disposition; @@ -45,68 +43,74 @@ const FlaggedPost = Post.extend({ return iconHTML(icon, { title }); }, - wasEdited: function () { + @computed('last_revised_at', 'post_actions.@each.created_at') + wasEdited(lastRevisedAt) { if (Ember.isEmpty(this.get("last_revised_at"))) { return false; } - var lastRevisedAt = Date.parse(this.get("last_revised_at")); + lastRevisedAt = Date.parse(lastRevisedAt); return _.some(this.get("post_actions"), function (postAction) { return Date.parse(postAction.created_at) < lastRevisedAt; }); - }.property("last_revised_at", "post_actions.@each.created_at"), + }, - conversations: function () { - var self = this; - var conversations = []; + @computed + conversations() { + let conversations = []; - _.each(this.post_actions, function (postAction) { + this.post_actions.forEach(postAction => { if (postAction.conversation) { - var conversation = { + let conversation = { permalink: postAction.permalink, hasMore: postAction.conversation.has_more, response: { excerpt: postAction.conversation.response.excerpt, - user: self.userLookup[postAction.conversation.response.user_id] + user: this.userLookup[postAction.conversation.response.user_id] } }; if (postAction.conversation.reply) { - conversation["reply"] = { + conversation.reply = { excerpt: postAction.conversation.reply.excerpt, - user: self.userLookup[postAction.conversation.reply.user_id] + user: this.userLookup[postAction.conversation.reply.user_id] }; } - conversations.push(conversation); } }); return conversations; - }.property(), + }, - user: function() { + @computed + user() { return this.userLookup[this.user_id]; - }.property(), + }, - topic: function () { + @computed + topic() { return this.topicLookup[this.topic_id]; - }.property(), + }, - flaggedForSpam: function() { + @computed('post_actions.@each.name_key') + flaggedForSpam() { return !_.every(this.get('post_actions'), function(action) { return action.name_key !== 'spam'; }); - }.property('post_actions.@each.name_key'), + }, - topicFlagged: function() { + @computed('post_actions.@each.targets_topic') + topicFlagged() { return _.any(this.get('post_actions'), function(action) { return action.targets_topic; }); - }.property('post_actions.@each.targets_topic'), + }, - postAuthorFlagged: function() { + @computed('post_actions.@each.targets_topic') + postAuthorFlagged() { return _.any(this.get('post_actions'), function(action) { return !action.targets_topic; }); - }.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'); - }.property('flaggedForSpam'), + @computed('flaggedForSpan') + canDeleteAsSpammer(flaggedForSpam) { + return Discourse.User.currentProp('staff') && flaggedForSpam && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted'); + }, - deletePost: function() { + deletePost() { if (this.get('post_number') === 1) { return ajax('/t/' + this.topic_id, { type: 'DELETE', cache: false }); } else { @@ -114,61 +118,58 @@ const FlaggedPost = Post.extend({ } }, - disagreeFlags: function () { + disagreeFlags() { return ajax('/admin/flags/disagree/' + this.id, { type: 'POST', cache: false }); }, - deferFlags: function (deletePost) { + deferFlags(deletePost) { return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }); }, - agreeFlags: function (actionOnPost) { + agreeFlags(actionOnPost) { return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }); }, - postHidden: Em.computed.alias('hidden'), + postHidden: Ember.computed.alias('hidden'), - extraClasses: function() { - var classes = []; + @computed + extraClasses() { + let classes = []; if (this.get('hidden')) { classes.push('hidden-post'); } if (this.get('deleted')) { classes.push('deleted'); } return classes.join(' '); - }.property(), - - deleted: Em.computed.or('deleted_at', 'topic_deleted_at') + }, + deleted: Ember.computed.or('deleted_at', 'topic_deleted_at') }); FlaggedPost.reopenClass({ - findAll: function (filter, offset) { + + findAll(args) { + let { offset, filter } = args; offset = offset || 0; - var result = Em.A(); + let result = []; result.set('loading', true); return ajax('/admin/flags/' + filter + '.json?offset=' + offset).then(function (data) { // users - var userLookup = {}; - _.each(data.users, function (user) { - userLookup[user.id] = AdminUser.create(user); - }); + let userLookup = {}; + data.users.forEach(user => userLookup[user.id] = AdminUser.create(user)); // topics - var topicLookup = {}; - _.each(data.topics, function (topic) { - topicLookup[topic.id] = Topic.create(topic); - }); + let topicLookup = {}; + data.topics.forEach(topic => topicLookup[topic.id] = Topic.create(topic)); // posts - _.each(data.posts, function (post) { - var f = FlaggedPost.create(post); + data.posts.forEach(post => { + let f = 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-topics-index.js.es6 b/app/assets/javascripts/admin/routes/admin-flags-topics-index.js.es6 new file mode 100644 index 00000000000..bb18c6c56ac --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-flags-topics-index.js.es6 @@ -0,0 +1,9 @@ +export default Discourse.Route.extend({ + model() { + return this.store.findAll('flagged-topic'); + }, + + setupController(controller, model) { + controller.set('flaggedTopics', model); + } +}); 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 new file mode 100644 index 00000000000..66b87352268 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-flags-topics-show.js.es6 @@ -0,0 +1,18 @@ +import { loadTopicView } from 'discourse/models/topic'; +import FlaggedPost from 'admin/models/flagged-post'; + +export default Ember.Route.extend({ + model(params) { + let topicRecord = this.store.createRecord('topic', { id: params.id }); + let topic = loadTopicView(topicRecord).then(() => topicRecord); + + return Ember.RSVP.hash({ + topic, + flaggedPosts: FlaggedPost.findAll({ filter: 'active' }) + }); + }, + + setupController(controller, hash) { + controller.setProperties(hash); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index 0644cd25524..7628ca83895 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -56,6 +56,9 @@ export default function() { this.route('adminFlags', { path: '/flags', resetNamespace: true }, function() { this.route('postsActive', { path: 'active' }); this.route('postsOld', { path: 'old' }); + this.route('topics', { path: 'topics' }, function() { + this.route('show', { path: ":id" }); + }); }); this.route('adminLogs', { path: '/logs', resetNamespace: true }, function() { diff --git a/app/assets/javascripts/admin/templates/components/flag-counts.hbs b/app/assets/javascripts/admin/templates/components/flag-counts.hbs new file mode 100644 index 00000000000..ff1f0118258 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/flag-counts.hbs @@ -0,0 +1,2 @@ +{{title}} +x{{details.count}} diff --git a/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs b/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs new file mode 100644 index 00000000000..dfc4b3da425 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs @@ -0,0 +1,5 @@ +{{#each users as |u|}} + {{#link-to 'adminUser' u}} + {{avatar u imageSize="small"}} + {{/link-to}} +{{/each}} diff --git a/app/assets/javascripts/admin/templates/flags-topics-index.hbs b/app/assets/javascripts/admin/templates/flags-topics-index.hbs new file mode 100644 index 00000000000..110df355f62 --- /dev/null +++ b/app/assets/javascripts/admin/templates/flags-topics-index.hbs @@ -0,0 +1,42 @@ +{{plugin-outlet name="flagged-topics-before" noTags=true args=(hash flaggedTopics=flaggedTopics)}} + + + + {{plugin-outlet name="flagged-topic-header-row" noTags=true}} + + + + + + + + {{#each flaggedTopics as |ft|}} + + {{plugin-outlet name="flagged-topic-row" noTags=true args=(hash topic=ft.topic)}} + + + + + + + + {{/each}} +
{{i18n "admin.flags.flagged_topics.topic"}} {{i18n "admin.flags.flagged_topics.type"}}{{I18n "admin.flags.flagged_topics.users"}}{{i18n "admin.flags.flagged_topics.last_flagged"}}
+ {{replace-emoji ft.topic.fancy_title}} + + {{#each ft.flag_counts as |fc|}} + {{flag-counts details=fc}} + {{/each}} + + {{flagged-topic-users users=ft.users tagName=""}} + + {{format-age ft.last_flag_at}} + + {{#link-to + "adminFlags.topics.show" + ft.id + class="btn d-button no-text btn-small btn-primary" + title=(i18n "admin.flags.show_details")}} + {{d-icon "search"}} + {{/link-to}} +
diff --git a/app/assets/javascripts/admin/templates/flags-topics-show.hbs b/app/assets/javascripts/admin/templates/flags-topics-show.hbs new file mode 100644 index 00000000000..95bba4113a2 --- /dev/null +++ b/app/assets/javascripts/admin/templates/flags-topics-show.hbs @@ -0,0 +1,11 @@ +
+
+ {{topic-status topic=topic}} +

{{{topic.fancyTitle}}}

+
+ + {{plugin-outlet name="flagged-topic-details-header" args=(hash topic=topic)}} +
+
+ {{flagged-posts flaggedPosts=flaggedPosts filter="active"}} +
diff --git a/app/assets/javascripts/admin/templates/flags.hbs b/app/assets/javascripts/admin/templates/flags.hbs index f794efb57b4..e24c12d8ec3 100644 --- a/app/assets/javascripts/admin/templates/flags.hbs +++ b/app/assets/javascripts/admin/templates/flags.hbs @@ -1,6 +1,7 @@ {{#admin-nav}} - {{nav-item route='adminFlags.postsActive' label='admin.flags.active'}} - {{nav-item route='adminFlags.postsOld' label='admin.flags.old'}} + {{nav-item route='adminFlags.postsActive' label='admin.flags.active_posts'}} + {{nav-item route='adminFlags.topics' label='admin.flags.topics'}} + {{nav-item route='adminFlags.postsOld' label='admin.flags.old_posts' class='right'}} {{/admin-nav}}
diff --git a/app/assets/javascripts/discourse-common/resolver.js.es6 b/app/assets/javascripts/discourse-common/resolver.js.es6 index 1bbc1b432bf..4c2dec2f0bf 100644 --- a/app/assets/javascripts/discourse-common/resolver.js.es6 +++ b/app/assets/javascripts/discourse-common/resolver.js.es6 @@ -194,7 +194,6 @@ export function buildResolver(baseName) { // (similar to how discourse lays out templates) findAdminTemplate(parsedName) { var decamelized = parsedName.fullNameWithoutType.decamelize(); - if (decamelized.indexOf('components') === 0) { const compTemplate = Ember.TEMPLATES['admin/templates/' + decamelized]; if (compTemplate) { return compTemplate; } diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index b56ac1628aa..af0da8debe2 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,7 +1,14 @@ import { ajax } from 'discourse/lib/ajax'; import { hashString } from 'discourse/lib/hash'; -const ADMIN_MODELS = ['plugin', 'theme', 'embeddable-host', 'web-hook', 'web-hook-event']; +const ADMIN_MODELS = [ + 'plugin', + 'theme', + 'embeddable-host', + 'web-hook', + 'web-hook-event', + 'flagged-topic' +]; export function Result(payload, responseJson) { this.payload = payload; @@ -19,7 +26,6 @@ function rethrow(error) { export default Ember.Object.extend({ - storageKey(type, findArgs, options) { if (options && options.cacheKey) { return options.cacheKey; diff --git a/app/assets/javascripts/discourse/components/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/components/plugin-outlet.js.es6 index 268e964edab..396d8b97515 100644 --- a/app/assets/javascripts/discourse/components/plugin-outlet.js.es6 +++ b/app/assets/javascripts/discourse/components/plugin-outlet.js.es6 @@ -36,6 +36,12 @@ export default Ember.Component.extend({ connectors: null, init() { + // This should be the future default + if (this.get('noTags')) { + this.set('tagName', ''); + this.set('connectorTagName', ''); + } + this._super(); const name = this.get('name'); if (name) { diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index eedbf6a0b77..e398bf7f2b3 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -4,6 +4,7 @@ @import "common/foundation/helpers"; @import "common/admin/customize"; +@import "common/admin/flagged_topics"; $mobile-breakpoint: 700px; diff --git a/app/assets/stylesheets/common/admin/flagged_topics.scss b/app/assets/stylesheets/common/admin/flagged_topics.scss new file mode 100644 index 00000000000..23f56b76e8d --- /dev/null +++ b/app/assets/stylesheets/common/admin/flagged_topics.scss @@ -0,0 +1,28 @@ +.flag-counts { + display: inline-block; + margin-right: 0.5em; + + .type-count { + color: $primary-medium; + font-size: 0.9em; + } +} + +.flagged-topic { + td.topic-title { + width: 400px; + a { + width: 400px; + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } +} + +.flagged-topic-details { + display: flex; + justify-content: space-between; + +} diff --git a/app/controllers/admin/flagged_topics_controller.rb b/app/controllers/admin/flagged_topics_controller.rb new file mode 100644 index 00000000000..d84d89680c3 --- /dev/null +++ b/app/controllers/admin/flagged_topics_controller.rb @@ -0,0 +1,14 @@ +require_dependency 'flag_query' + +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) + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dbaa21b76c3..50a36187232 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -490,7 +490,7 @@ class ApplicationController < ActionController::Base # status - HTTP status code to return def render_json_error(obj, opts = {}) opts = { status: opts } if opts.is_a?(Integer) - render json: MultiJson.dump(create_errors_json(obj, opts[:type])), status: opts[:status] || 422 + render json: MultiJson.dump(create_errors_json(obj, opts)), status: opts[:status] || 422 end def success_json diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index 25a6422b0e5..00f5cd9fb14 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -132,6 +132,7 @@ module HasCustomFields if @preloaded.key?(key) @preloaded[key] else + raise "#{@preloaded.inspect} -> #{key.inspect}" # for now you can not mix preload an non preload, it better just to fail raise StandardError, "Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries." end diff --git a/app/serializers/flagged_topic_summary_serializer.rb b/app/serializers/flagged_topic_summary_serializer.rb new file mode 100644 index 00000000000..de770fef25b --- /dev/null +++ b/app/serializers/flagged_topic_summary_serializer.rb @@ -0,0 +1,29 @@ +class FlaggedTopicSummarySerializer < ActiveModel::Serializer + + attributes( + :id, + :flag_counts, + :user_ids, + :last_flag_at + ) + + has_one :topic, serializer: FlaggedTopicSerializer + + def id + topic.id + end + + def flag_counts + object.flag_counts.map do |k, v| + { flag_type_id: k, count: v } + end + end + + def user_ids + object.user_ids + end + + def last_flag_at + object.last_flag_at + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 528cfea5f3c..c7d6fc8ed84 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2596,8 +2596,9 @@ en: flags: title: "Flags" - old: "Old" - active: "Active" + active_posts: "Flagged Posts" + old_posts: "Old Flagged Posts" + topics: "Flagged Topics" agree: "Agree" agree_title: "Confirm this flag as valid and correct" @@ -2638,11 +2639,18 @@ en: system: "System" error: "Something went wrong" reply_message: "Reply" - no_results: "There are no flags." + no_results: "There are no flaged posts." topic_flagged: "This topic has been flagged." visit_topic: "Visit the topic to take action" was_edited: "Post was edited after the first flag" previous_flags_count: "This post has already been flagged {{count}} times." + show_details: "Show flag details" + + flagged_topics: + topic: "Topic" + type: "Type" + users: "Users" + last_flagged: "Last Flagged" summary: action_type_3: diff --git a/config/routes.rb b/config/routes.rb index 847f8ee083e..33389b5e107 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -188,11 +188,14 @@ Discourse::Application.routes.draw do get "flags" => "flags#index" get "flags/:filter" => "flags#index" + get "flags/topics/:topic_id" => "flags#index" post "flags/agree/:id" => "flags#agree" post "flags/disagree/:id" => "flags#disagree" post "flags/defer/:id" => "flags#defer" + resources :flagged_topics, constraints: AdminConstraint.new resources :themes, constraints: AdminConstraint.new + post "themes/import" => "themes#import" post "themes/upload_asset" => "themes#upload_asset" get "themes/:id/preview" => "themes#preview" diff --git a/lib/flag_query.rb b/lib/flag_query.rb index 6f99431d88b..50ea28dbe9f 100644 --- a/lib/flag_query.rb +++ b/lib/flag_query.rb @@ -1,3 +1,5 @@ +require 'ostruct' + module FlagQuery def self.flagged_posts_report(current_user, filter, offset = 0, per_page = 25) @@ -128,6 +130,44 @@ module FlagQuery end + def self.flagged_topics + + results = PostAction + .flags + .active + .includes(post: [:user, :topic]) + .order('post_actions.created_at DESC') + + ft_by_id = {} + users_by_id = {} + topics_by_id = {} + + results.each do |pa| + if pa.post.present? && pa.post.topic.present? + ft = ft_by_id[pa.post.topic.id] ||= OpenStruct.new( + topic: pa.post.topic, + flag_counts: {}, + user_ids: [], + last_flag_at: pa.created_at + ) + + topics_by_id[pa.post.topic.id] = pa.post.topic + + ft.flag_counts[pa.post_action_type_id] ||= 0 + ft.flag_counts[pa.post_action_type_id] += 1 + + ft.user_ids << pa.post.user_id + ft.user_ids.uniq! + + users_by_id[pa.post.user_id] ||= pa.post.user + end + end + + Topic.preload_custom_fields(topics_by_id.values, TopicList.preloaded_custom_fields) + + { flagged_topics: ft_by_id.values, users: users_by_id.values } + end + private def self.excerpt(cooked) diff --git a/lib/json_error.rb b/lib/json_error.rb index 5dfc720506c..1675ac562b2 100644 --- a/lib/json_error.rb +++ b/lib/json_error.rb @@ -1,8 +1,11 @@ module JsonError - def create_errors_json(obj, type = nil) + def create_errors_json(obj, opts = nil) + opts ||= {} + errors = create_errors_array obj - errors[:error_type] = type if type + errors[:error_type] = opts[:type] if opts[:type] + errors[:extras] = opts[:extras] if opts[:extras] errors end diff --git a/test/javascripts/acceptance/admin-flags-topics-test.js.es6 b/test/javascripts/acceptance/admin-flags-topics-test.js.es6 new file mode 100644 index 00000000000..23e6f56ddc2 --- /dev/null +++ b/test/javascripts/acceptance/admin-flags-topics-test.js.es6 @@ -0,0 +1,11 @@ +import { acceptance } from "helpers/qunit-helpers"; +acceptance("Admin - Flagged Topics", { loggedIn: true }); + +QUnit.test("topics with flags", assert => { + visit("/admin/flags/topics"); + andThen(() => { + assert.ok(exists('.watched-words-list')); + assert.ok(!exists('.watched-words-list .watched-word'), "Don't show bad words by default."); + }); +}); + diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index fe08188b940..a803bccfe43 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -323,6 +323,14 @@ export default function() { ]); }); + this.get('/admin/flagged_topics', () => { + return response(200, { + "flagged_topics": [ + { id: 1 } + ] + }); + }); + this.get('/admin/customize/site_texts', request => { if (request.queryParams.overridden) { diff --git a/test/javascripts/helpers/create-store.js.es6 b/test/javascripts/helpers/create-store.js.es6 index a1ab5351a81..8788af03ad4 100644 --- a/test/javascripts/helpers/create-store.js.es6 +++ b/test/javascripts/helpers/create-store.js.es6 @@ -13,7 +13,6 @@ export default function() { if (type === "adapter:rest") { if (!this._restAdapter) { this._restAdapter = RestAdapter.create({ owner: this }); - // this._restAdapter.container = this; } return this._restAdapter; } @@ -37,4 +36,4 @@ export default function() { }, } }); -} \ No newline at end of file +}