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}}
+ {{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"}} |
+ |
+
+
+ {{#each flaggedTopics as |ft|}}
+
+ {{plugin-outlet name="flagged-topic-row" noTags=true args=(hash topic=ft.topic)}}
+
+
+ {{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}}
+ |
+
+ {{/each}}
+
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
+}