Add front end service for staff controls

This commit is contained in:
Robin Ward 2017-09-12 13:04:53 -04:00
parent 5cf50f0034
commit d7c37d9369
31 changed files with 294 additions and 186 deletions

View File

@ -2,7 +2,9 @@ import RestAdapter from 'discourse/adapters/rest';
export default RestAdapter.extend({ export default RestAdapter.extend({
pathFor(store, type, findArgs) { 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) { afterFindAll(results, helper) {

View File

@ -1,8 +1,8 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality'; 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, removeAfter: null,
deleteSpammer: null,
actions: { actions: {
agreeDeleteSpammer(user) { agreeDeleteSpammer(user) {

View File

@ -1,15 +1,10 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality'; 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, removeAfter: null,
actions: { actions: {
deleteSpammer(user) {
return this.removeAfter(user.deleteAsSpammer()).then(() => {
this.send('closeModal');
});
},
deletePostDeferFlag() { deletePostDeferFlag() {
let flaggedPost = this.get('model'); let flaggedPost = this.get('model');
this.removeAfter(flaggedPost.deferFlags(true)).then(() => { this.removeAfter(flaggedPost.deferFlags(true)).then(() => {

View File

@ -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');
});
}
}
});

View File

@ -463,59 +463,6 @@ const AdminUser = Discourse.User.extend({
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" }); 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() { loadDetails() {
const user = this; const user = this;

View File

@ -1,5 +1,4 @@
import { loadTopicView } from 'discourse/models/topic'; import { loadTopicView } from 'discourse/models/topic';
import FlaggedPost from 'admin/models/flagged-post';
export default Ember.Route.extend({ export default Ember.Route.extend({
model(params) { model(params) {
@ -8,7 +7,7 @@ export default Ember.Route.extend({
return Ember.RSVP.hash({ return Ember.RSVP.hash({
topic, topic,
flaggedPosts: FlaggedPost.findAll({ flaggedPosts: this.store.findAll('flagged-post', {
filter: 'active', filter: 'active',
topic_id: params.id topic_id: params.id
}) })

View File

@ -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"});
});
});
}
});

View File

@ -10,6 +10,7 @@
{{plugin-outlet name="flagged-topic-details-header" args=(hash topic=topic)}} {{plugin-outlet name="flagged-topic-details-header" args=(hash topic=topic)}}
</div> </div>
<div class='topic-flags'> <div class='topic-flags'>
{{flagged-posts {{flagged-posts
flaggedPosts=flaggedPosts flaggedPosts=flaggedPosts

View File

@ -24,10 +24,10 @@
icon="thumbs-o-up" icon="thumbs-o-up"
label="admin.flags.agree_flag"}} label="admin.flags.agree_flag"}}
{{#if model.canDeleteAsSpammer}} {{#if canDeleteSpammer}}
{{d-button {{d-button
title="admin.flags.delete_spammer_title" title="admin.flags.delete_spammer_title"
action=(action "agreeDeleteSpammer" model.user) action="deleteSpammer"
class="btn-danger delete-spammer" class="btn-danger delete-spammer"
icon="exclamation-triangle" icon="exclamation-triangle"
label="admin.flags.delete_spammer"}} label="admin.flags.delete_spammer"}}

View File

@ -13,11 +13,11 @@
icon="thumbs-o-up" icon="thumbs-o-up"
label="admin.flags.delete_post_agree_flag"}} label="admin.flags.delete_post_agree_flag"}}
{{#if model.canDeleteAsSpammer}} {{#if canDeleteSpammer}}
{{d-button {{d-button
class="btn-danger delete-spammer" class="btn-danger delete-spammer"
title="admin.flags.delete_spammer_title" title="admin.flags.delete_spammer_title"
action=(action "deleteSpammer" model.user) action="deleteSpammer"
icon="exclamation-triangle" icon="exclamation-triangle"
label="admin.flags.delete_spammer"}} label="admin.flags.delete_spammer"}}
{{/if}} {{/if}}

View File

@ -1,5 +1,3 @@
import FlaggedPost from 'admin/models/flagged-post';
export default Ember.Component.extend({ export default Ember.Component.extend({
canAct: Ember.computed.equal('filter', 'active'), canAct: Ember.computed.equal('filter', 'active'),
showResolvedBy: Ember.computed.equal('filter', 'old'), showResolvedBy: Ember.computed.equal('filter', 'old'),
@ -11,28 +9,10 @@ export default Ember.Component.extend({
}, },
loadMore() { loadMore() {
if (this.get('allLoaded')) {
return;
}
const flaggedPosts = this.get('flaggedPosts'); const flaggedPosts = this.get('flaggedPosts');
if (flaggedPosts.get('canLoadMore')) {
let args = { flaggedPosts.loadMore();
filter: this.get('query'),
offset: flaggedPosts.length+1
};
let topic = this.get('topic');
if (topic) {
args.topic_id = topic.id;
} }
return FlaggedPost.findAll(args).then(data => {
if (data.length === 0) {
this.set('allLoaded', true);
}
flaggedPosts.addObjects(data);
});
} }
} }
}); });

View File

@ -3,17 +3,35 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ActionSummary from 'discourse/models/action-summary'; import ActionSummary from 'discourse/models/action-summary';
import { MAX_MESSAGE_LENGTH } from 'discourse/models/post-action-type'; import { MAX_MESSAGE_LENGTH } from 'discourse/models/post-action-type';
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
import optionalService from 'discourse/lib/optional-service';
export default Ember.Controller.extend(ModalFunctionality, { export default Ember.Controller.extend(ModalFunctionality, {
adminTools: optionalService(),
userDetails: null, userDetails: null,
selected: null, selected: null,
flagTopic: null, flagTopic: null,
message: null, message: null,
isWarning: false, isWarning: false,
topicActionByName: null, topicActionByName: null,
spammerDetails: null,
onShow() { 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') @computed('flagTopic')
@ -74,13 +92,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
submitDisabled: Em.computed.not('submitEnabled'), submitDisabled: Em.computed.not('submitEnabled'),
// Staff accounts can "take action" // Staff accounts can "take action"
canTakeAction: function() { @computed('flagTopic', 'selected.is_custom_flag')
if (this.get("flagTopic")) return false; canTakeAction(flagTopic, isCustomFlag) {
return !flagTopic && !isCustomFlag && this.currentUser.get('staff');
// 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'),
submitText: function(){ submitText: function(){
if (this.get('selected.is_custom_flag')) { if (this.get('selected.is_custom_flag')) {
@ -91,6 +106,13 @@ export default Ember.Controller.extend(ModalFunctionality, {
}.property('selected.is_custom_flag'), }.property('selected.is_custom_flag'),
actions: { actions: {
deleteSpammer() {
let details = this.get('spammerDetails');
if (details) {
details.deleteUser().then(() => window.location.reload());
}
},
takeAction() { takeAction() {
this.send('createFlag', {takeAction: true}); this.send('createFlag', {takeAction: true});
this.set('model.hidden', 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') @computed('flagTopic', 'selected.name_key')
canSendWarning(flagTopic, nameKey) { canSendWarning(flagTopic, nameKey) {
return !flagTopic && this.currentUser.get('staff') && nameKey === 'notify_user'; 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));
}
} }
}); });

View File

@ -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)}`);
});
};

View File

@ -145,10 +145,12 @@ export default Ember.Object.extend({
const self = this; const self = this;
return ajax(url).then(function(result) { return ajax(url).then(function(result) {
const typeName = Ember.String.underscore(self.pluralize(type)), let typeName = Ember.String.underscore(self.pluralize(type));
totalRows = result["total_rows_" + typeName] || result.get('totalRows'),
loadMoreUrl = result["load_more_" + typeName], let pageTarget = result.meta || result;
content = result[typeName].map(obj => self._hydrate(type, obj, 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.setProperties({ totalRows, loadMoreUrl });
resultSet.get('content').pushObjects(content); resultSet.get('content').pushObjects(content);
@ -192,12 +194,14 @@ export default Ember.Object.extend({
const typeName = Ember.String.underscore(this.pluralize(type)); const typeName = Ember.String.underscore(this.pluralize(type));
const content = result[typeName].map(obj => this._hydrate(type, obj, result)); const content = result[typeName].map(obj => this._hydrate(type, obj, result));
let pageTarget = result.meta || result;
const createArgs = { const createArgs = {
content, content,
findArgs, findArgs,
totalRows: result["total_rows_" + typeName] || content.length, totalRows: pageTarget["total_rows_" + typeName] || content.length,
loadMoreUrl: result["load_more_" + typeName], loadMoreUrl: pageTarget["load_more_" + typeName],
refreshUrl: result['refresh_' + typeName], refreshUrl: pageTarget['refresh_' + typeName],
store: this, store: this,
__type: type __type: type
}; };

View File

@ -30,7 +30,7 @@ export default {
DiscourseURL.appEvents = appEvents; DiscourseURL.appEvents = appEvents;
app.register('store:main', Store); app.register('store:main', Store);
inject(app, 'store', 'route', 'controller'); inject(app, 'store', 'route', 'controller', 'service');
const messageBus = window.MessageBus; const messageBus = window.MessageBus;
app.register('message-bus:main', messageBus, { instantiate: false }); app.register('message-bus:main', messageBus, { instantiate: false });

View File

@ -138,10 +138,6 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
}); });
}, },
deleteSpammer(user) {
user.deleteAsSpammer.then(() => window.location.reload());
},
checkEmail(user) { checkEmail(user) {
user.checkEmail(); user.checkEmail();
}, },

View File

@ -41,13 +41,13 @@ const TopicRoute = Discourse.Route.extend({
showFlags(model) { showFlags(model) {
let controller = showModal('flag', { model }); let controller = showModal('flag', { model });
controller.setProperties({ selected: null, flagTopic: false }); controller.setProperties({ flagTopic: false });
}, },
showFlagTopic() { showFlagTopic() {
const model = this.modelFor('topic'); const model = this.modelFor('topic');
let controller = showModal('flag', { model }); let controller = showModal('flag', { model });
controller.setProperties({ selected: null, flagTopic: true }); controller.setProperties({ flagTopic: true });
}, },
showTopicStatusUpdate() { showTopicStatusUpdate() {

View File

@ -39,10 +39,10 @@
label="flagging.take_action"}} label="flagging.take_action"}}
{{/if}} {{/if}}
{{#if canDeleteSpammer}} {{#if showDeleteSpammer}}
{{d-button {{d-button
class="btn-danger" class="btn-danger"
action=(route-action "deleteSpammer" userDetails) action="deleteSpammer"
disabled=submitDisabled disabled=submitDisabled
icon="exclamation-triangle" icon="exclamation-triangle"
label="flagging.delete_spammer"}} label="flagging.delete_spammer"}}

View File

@ -152,6 +152,7 @@
.flagged-topic-details { .flagged-topic-details {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 2em;
} }
.delete-flag-modal, .agree-flag-modal { .delete-flag-modal, .agree-flag-modal {

View File

@ -5,10 +5,13 @@ class Admin::FlaggedTopicsController < Admin::AdminController
def index def index
result = FlagQuery.flagged_topics result = FlagQuery.flagged_topics
render_json_dump({ render_json_dump(
{
flagged_topics: serialize_data(result[:flagged_topics], FlaggedTopicSummarySerializer), flagged_topics: serialize_data(result[:flagged_topics], FlaggedTopicSummarySerializer),
users: serialize_data(result[:users], BasicUserSerializer), users: serialize_data(result[:users], BasicUserSerializer),
}, rest_serializer: true) },
rest_serializer: true
)
end end
end end

View File

@ -10,19 +10,35 @@ class Admin::FlagsController < Admin::AdminController
# we may get out of sync, fix it here # we may get out of sync, fix it here
PostAction.update_flagged_posts_count 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, current_user,
filter: params[:filter], filter: params[:filter],
offset: params[:offset].to_i, offset: offset,
topic_id: params[:topic_id], topic_id: params[:topic_id],
per_page: Admin::FlagsController.flags_per_page, per_page: per_page,
rest_api: params[:rest_api].present? rest_api: params[:rest_api].present?
) )
if posts.blank?
render json: { posts: [], topics: [], users: [] }
else
if params[:rest_api] 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( render_json_dump(
{ {
flagged_posts: posts, flagged_posts: posts,
@ -31,11 +47,7 @@ class Admin::FlagsController < Admin::AdminController
post_actions: post_actions post_actions: post_actions
}, },
rest_serializer: true, rest_serializer: true,
meta: { meta: meta
types: {
disposed_by: 'user'
}
}
) )
else else
render_json_dump( render_json_dump(
@ -45,7 +57,6 @@ class Admin::FlagsController < Admin::AdminController
) )
end end
end end
end
def agree def agree
params.permit(:id, :action_on_post) params.permit(:id, :action_on_post)

View File

@ -317,7 +317,7 @@ class Post < ActiveRecord::Base
end end
def archetype def archetype
topic.archetype topic&.archetype
end end
def self.regular_order def self.regular_order

View File

@ -272,7 +272,7 @@ class Topic < ActiveRecord::Base
end end
def has_flags? def has_flags?
FlagQuery.flagged_post_actions("active") FlagQuery.flagged_post_actions(filter: "active")
.where("topics.id" => id) .where("topics.id" => id)
.exists? .exists?
end end

View File

@ -187,7 +187,7 @@ Discourse::Application.routes.draw do
put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new
get "flags" => "flags#index" get "flags" => "flags#index"
get "flags/:filter" => "flags#index" get "flags/:filter" => "flags#index", as: 'flags_filtered'
get "flags/topics/:topic_id" => "flags#index" get "flags/topics/:topic_id" => "flags#index"
post "flags/agree/:id" => "flags#agree" post "flags/agree/:id" => "flags#agree"
post "flags/disagree/:id" => "flags#disagree" post "flags/disagree/:id" => "flags#disagree"

View File

@ -4,10 +4,8 @@ module FlagQuery
def self.flagged_posts_report(current_user, opts = nil) def self.flagged_posts_report(current_user, opts = nil)
opts ||= {} opts ||= {}
filter = opts[:filter] || 'active'
offset = opts[:offset] || 0 offset = opts[:offset] || 0
per_page = opts[:per_page] || 25 per_page = opts[:per_page] || 25
topic_id = opts[:topic_id]
actions = flagged_post_actions(opts) actions = flagged_post_actions(opts)
@ -21,6 +19,8 @@ module FlagQuery
) )
end end
total_rows = actions.count
post_ids = actions.limit(per_page) post_ids = actions.limit(per_page)
.offset(offset) .offset(offset)
.group(:post_id) .group(:post_id)
@ -28,8 +28,6 @@ module FlagQuery
.pluck(:post_id) .pluck(:post_id)
.uniq .uniq
return nil if post_ids.blank?
posts = SqlBuilder.new(" posts = SqlBuilder.new("
SELECT p.id, SELECT p.id,
p.cooked, p.cooked,
@ -129,11 +127,14 @@ module FlagQuery
posts, posts,
Topic.with_deleted.where(id: topic_ids.to_a).to_a, Topic.with_deleted.where(id: topic_ids.to_a).to_a,
User.includes(:user_stat).where(id: user_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 end
def self.flagged_post_actions(opts) def self.flagged_post_actions(opts = nil)
opts ||= {}
post_actions = PostAction.flags post_actions = PostAction.flags
.joins("INNER JOIN posts ON posts.id = post_actions.post_id") .joins("INNER JOIN posts ON posts.id = post_actions.post_id")
.joins("INNER JOIN topics ON topics.id = posts.topic_id") .joins("INNER JOIN topics ON topics.id = posts.topic_id")

View File

@ -57,7 +57,7 @@ describe FlagQuery do
posts = FlagQuery.flagged_posts_report(admin, topic_id: post.topic_id) posts = FlagQuery.flagged_posts_report(admin, topic_id: post.topic_id)
expect(posts).to be_present expect(posts).to be_present
posts = FlagQuery.flagged_posts_report(admin, topic_id: -1) 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 # chuck post in category a mod can not see and make sure its missing
category = Fabricate(:category) category = Fabricate(:category)

View File

@ -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

View File

@ -6,6 +6,8 @@ QUnit.test("flagged posts", assert => {
andThen(() => { andThen(() => {
assert.equal(find('.flagged-posts .flagged-post').length, 1); 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 .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);
}); });
}); });

View File

@ -49,7 +49,17 @@ export default function(helpers) {
id: 1, id: 1,
user_id: eviltrout.id, user_id: eviltrout.id,
post_action_type_id: 8, 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" "__rest_serializer": "1"
}); });

View File

@ -28,7 +28,26 @@ export default function(helpers) {
}); });
this.get('/fruits', function() { 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) { 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)); } if (qp.id) { result = result.filterBy('id', parseInt(qp.id)); }
} }
return response({ widgets: result, return response({
widgets: result,
meta: {
total_rows_widgets: 4, total_rows_widgets: 4,
load_more_widgets: '/load-more-widgets', load_more_widgets: '/load-more-widgets',
refresh_widgets: '/widgets?refresh=true' }); refresh_widgets: '/widgets?refresh=true'
}
});
}); });
this.get('/load-more-widgets', function() { this.get('/load-more-widgets', function() {

View File

@ -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) { QUnit.test('findAll embedded', function(assert) {
const store = createStore(); const store = createStore();
return store.findAll('fruit').then(function(fruits) { return store.findAll('fruit').then(function(fruits) {