Merge branch 'master' into story-138187767-mod-flag-names

This commit is contained in:
gaba
2017-03-07 13:04:28 +01:00
135 changed files with 3248 additions and 1534 deletions
+29 -1
View File
@@ -39,7 +39,7 @@ const getCountsByAssetID = (context, asset_ids) => {
/**
* Returns the comment count for all comments that are public based on their
* parent ids.
* @param {Object} context graph context
*
* @param {Array<String>} parent_ids the ids of parents for which there are
* comments that we want to get
*/
@@ -270,6 +270,33 @@ const genRecentComments = (_, ids) => {
.then(util.arrayJoinBy(ids, 'asset_id'));
};
/**
* genComments returns the comments by the id's. Only admins can see non-public comments.
* @param {Object} context graph context
* @param {Array<String>} ids the comment id's to fetch
* @return {Promise} resolves to the comments
*/
const genComments = ({user}, ids) => {
let comments;
if (user && user.hasRoles('ADMIN')) {
comments = CommentModel.find({
id: {
$in: ids
}
});
} else {
comments = CommentModel.find({
id: {
$in: ids
},
status: {
$in: ['NONE', 'ACCEPTED']
}
});
}
return comments.then(util.singleJoinBy(ids, 'id'));
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
@@ -277,6 +304,7 @@ const genRecentComments = (_, ids) => {
*/
module.exports = (context) => ({
Comments: {
get: new DataLoader((ids) => genComments(context, ids)),
getByQuery: (query) => getCommentsByQuery(context, query),
getCountByQuery: (query) => getCommentCountByQuery(context, query),
countByAssetID: new util.SharedCacheDataLoader('Comments.countByAssetID', 3600, (ids) => getCountsByAssetID(context, ids)),
+96 -34
View File
@@ -2,10 +2,12 @@ const _ = require('lodash');
const DataLoader = require('dataloader');
const {objectCacheKeyFn} = require('./util');
const CommentModel = require('../../models/comment');
const ActionModel = require('../../models/action');
const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
/**
* Returns a list of assets with action metadata included on the models.
*/
const getAssetMetrics = ({loaders: {Metrics, Assets, Comments}}, {from, to, sort, limit}) => {
let commentMetrics = {};
let assetMetrics = [];
@@ -14,6 +16,10 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
.then((actionSummaries) => {
commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
if (action_type !== sort) {
return acc;
}
if (!(item_id in acc)) {
acc[item_id] = [];
}
@@ -24,10 +30,10 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
}, {});
// Collect just the comment id's.
let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id));
let commentIDs = Object.keys(commentMetrics);
// Find those comments.
return Metrics.getSpecificComments.loadMany(commentIDs);
return Comments.get.loadMany(commentIDs);
})
.then((comments) => {
@@ -44,29 +50,24 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
}));
return {action_summaries, id: asset_id};
});
})
.filter((asset) => {
let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sort));
if (contextActionSummary === null || contextActionSummary.actionCount === 0) {
return false;
}
return true;
})
// Sort these metrics by the predefined sort order. This will ensure that
// if the action summary does not exist on the object, that it is less
// prefered over the one that does have it.
assetMetrics.sort((a, b) => {
.sort((a, b) => {
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort));
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort));
// If either a or b don't have this action type, then one of them will
// automatically win.
if (aActionSummary == null || bActionSummary == null) {
if (bActionSummary != null) {
return 1;
}
if (aActionSummary != null) {
return -1;
}
return 0;
}
// Both of them had an actionCount, hence we can determine that we could
// compare the actual values directly.
return bActionSummary.actionCount - aActionSummary.actionCount;
@@ -99,6 +100,75 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
});
};
/**
* Returns a list of comments that are retrieved based on most activity within
* the indicated time range.
*/
const getCommentMetrics = ({loaders: {Metrics, Comments}}, {from, to, sort, limit}) => {
let commentActionSummaries = {};
return Metrics.getRecentActions.load({from, to})
.then((actionSummaries) => {
actionSummaries.sort((a, b) => {
let aActionSummary = a.action_type === sort ? a : null;
let bActionSummary = b.action_type === sort ? b : null;
// If either a or b don't have this action type, then one of them will
// automatically win.
if (aActionSummary == null || bActionSummary == null) {
if (bActionSummary != null) {
return 1;
}
if (aActionSummary != null) {
return -1;
}
return 0;
}
// Both of them had an actionCount, hence we can determine that we could
// compare the actual values directly.
return bActionSummary.count - aActionSummary.count;
});
commentActionSummaries = _.groupBy(actionSummaries, 'item_id');
// Grab the comment id's for comment where they have at least one of the
// actions being sorted by.
let commentIDs = Object.keys(commentActionSummaries).filter((item_id) => {
let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sort);
if (contextActionSummary == null) {
return false;
}
return true;
});
// Only keep the top `limit`.
commentIDs = commentIDs.slice(0, limit);
// If there are no comment's to get, then just continue with an empty
// array.
if (commentIDs.length === 0) {
return [];
}
// Find those comments, this is the final stage, so let's get all the
// fields.
return Comments.get.loadMany(commentIDs);
})
.then((comments) => comments.map((comment) => {
// Add in the action summaries genrerated.
comment.action_summaries = commentActionSummaries[comment.id];
return comment;
}));
};
const getRecentActions = (context, {from, to}) => {
return ActionModel.aggregate([
@@ -131,25 +201,17 @@ const getRecentActions = (context, {from, to}) => {
]);
};
const getSpecificComments = (context, ids) => {
return CommentModel.find({
id: {
$in: ids
}
})
.select({
id: 1,
asset_id: 1
});
};
module.exports = (context) => ({
Metrics: {
getSpecificComments: new DataLoader((ids) => getSpecificComments(context, ids)),
getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
batch: false,
cacheKeyFn: objectCacheKeyFn('from', 'to')
}),
get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit})
Assets: {
get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit})
},
Comments: {
get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit})
}
}
});
+32 -2
View File
@@ -1,6 +1,7 @@
const errors = require('../../errors');
const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const CommentsService = require('../../services/comments');
const Wordlist = require('../../services/wordlist');
@@ -146,10 +147,11 @@ const createPublicComment = (context, commentInput) => {
// TODO: this is kind of fragile, we should refactor this to resolve
// all these const's that we're using like 'COMMENTS', 'FLAG' to be
// defined in a checkable schema.
return context.mutators.Action.create({
return ActionsService.insertUserAction({
item_id: comment.id,
item_type: 'COMMENTS',
action_type: 'FLAG',
user_id: null,
group_id: 'Matched suspect word filter',
metadata: {}
})
@@ -187,11 +189,31 @@ const setCommentStatus = ({loaders: {Comments}}, {id, status}) => {
});
};
/**
* Adds a tag to a Comment
* @param {String} id identifier of the comment (uuid)
* @param {String} tag name of the tag
*/
const addCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
return CommentsService.addTag(id, tag, user.id);
};
/**
* Removes a tag from a Comment
* @param {String} id identifier of the comment (uuid)
* @param {String} tag name of the tag
*/
const removeCommentTag = ({user, loaders: {Comments}}, {id, tag}) => {
return CommentsService.removeTag(id, tag);
};
module.exports = (context) => {
let mutators = {
Comment: {
create: () => Promise.reject(errors.ErrNotAuthorized),
setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized)
setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized),
addCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
removeCommentTag: () => Promise.reject(errors.ErrNotAuthorized),
}
};
@@ -203,5 +225,13 @@ module.exports = (context) => {
mutators.Comment.setCommentStatus = (action) => setCommentStatus(context, action);
}
if (context.user && context.user.can('mutation:addCommentTag')) {
mutators.Comment.addCommentTag = (action) => addCommentTag(context, action);
}
if (context.user && context.user.can('mutation:removeCommentTag')) {
mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action);
}
return mutators;
};
+12 -1
View File
@@ -1,4 +1,11 @@
const Comment = {
parent({parent_id}, _, {loaders: {Comments}}) {
if (parent_id == null) {
return null;
}
return Comments.get.load(parent_id);
},
user({author_id}, _, {loaders: {Users}}) {
return Users.getByID.load(author_id);
},
@@ -25,7 +32,11 @@ const Comment = {
return null;
},
action_summaries({id}, _, {loaders: {Actions}}) {
action_summaries({id, action_summaries}, _, {loaders: {Actions}}) {
if (action_summaries) {
return action_summaries;
}
return Actions.getSummariesByItemID.load(id);
},
asset({asset_id}, _, {loaders: {Assets}}) {
+8 -1
View File
@@ -1,5 +1,6 @@
const {Error: {ValidationError}} = require('mongoose');
const errors = require('../../errors');
const CommentsService = require('../../services/comments');
/**
* Wraps up a promise to return an object with the resolution of the promise
@@ -51,7 +52,13 @@ const RootMutation = {
},
setCommentStatus(_, {id, status}, {mutators: {Comment}}) {
return wrapResponse(null)(Comment.setCommentStatus({id, status}));
}
},
addCommentTag(_, {id, tag}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.addCommentTag({id, tag}).then(() => CommentsService.findById(id)));
},
removeCommentTag(_, {id, tag}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.removeCommentTag({id, tag}).then(() => CommentsService.findById(id)));
},
};
module.exports = RootMutation;
+13 -3
View File
@@ -38,7 +38,9 @@ const RootQuery = {
return Comments.getByQuery(query);
},
comment(_, {id}, {loaders: {Comments}}) {
return Comments.get.load(id);
},
commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) {
if (user == null || !user.hasRoles('ADMIN')) {
return null;
@@ -56,12 +58,20 @@ const RootQuery = {
return Comments.getCountByQuery({statuses, asset_id, parent_id});
},
metrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics}}) {
assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) {
if (user == null || !user.hasRoles('ADMIN')) {
return null;
}
return Metrics.get({from, to, sort, limit});
return Assets.get({from, to, sort, limit});
},
commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) {
if (user == null || !user.hasRoles('ADMIN')) {
return null;
}
return Comments.get({from, to, sort, limit});
},
// This returns the current user, ensure that if we aren't logged in, we
+1 -1
View File
@@ -15,7 +15,7 @@ const User = {
// If the user is not an admin, only return comment list for the owner of
// the comments.
if (user && (user.hasRoles('ADMIN') || user.id === id)) {
return Comments.getByQuery({author_id: id});
return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'});
}
return null;
+35 -5
View File
@@ -47,7 +47,7 @@ type User {
# returns all users based on a query.
users(query: UsersQuery): [User]
# returns user status
status: USER_STATUS
}
@@ -166,6 +166,9 @@ input CommentCountQuery {
# Comment is the base representation of user interaction in Talk.
type Comment {
# The parent of the comment (if there is one).
parent: Comment
# The ID of the comment.
id: ID!
@@ -494,6 +497,9 @@ type RootQuery {
# Site wide settings and defaults.
settings: Settings
# Finds a specific comment based on it's id.
comment(id: ID!): Comment
# All assets. Requires the `ADMIN` role.
assets: [Asset]
@@ -511,12 +517,16 @@ type RootQuery {
# role.
me: User
# Metrics related to user actions are saturated into the assets returned. The
# sort will affect if it will allow
metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset]
# Users returned based on a query.
users(query: UsersQuery): [User]
# Asset metrics related to user actions are saturated into the assets
# returned.
assetMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset!]
# Comment metrics related to user actions are saturated into the comments
# returned.
commentMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Comment!]
}
################################################################################
@@ -659,6 +669,20 @@ type SetCommentStatusResponse implements Response {
errors: [UserError]
}
# Response to addCommentTag mutation
type AddCommentTagResponse implements Response {
# An array of errors relating to the mutation that occured.
comment: Comment
errors: [UserError]
}
# Response to removeCommentTag mutation
type RemoveCommentTagResponse implements Response {
# An array of errors relating to the mutation that occured.
comment: Comment
errors: [UserError]
}
# All mutations for the application are defined on this object.
type RootMutation {
@@ -685,6 +709,12 @@ type RootMutation {
# Sets Comment status. Requires the `ADMIN` role.
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
# Add tag to comment.
addCommentTag(id: ID!, tag: String!): AddCommentTagResponse
# Remove tag from comment.
removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse
}
################################################################################