Merge branch 'next' into user-status-refactor

This commit is contained in:
Wyatt Johnson
2017-12-04 15:42:57 -07:00
62 changed files with 710 additions and 1222 deletions
-6
View File
@@ -79,11 +79,6 @@ const findOrCreateAssetByURL = async (context, asset_url) => {
return asset;
};
const getAssetsForMetrics = async ({loaders: {Comments}}) => {
return Comments.getByQuery({action_type: 'FLAG'})
.then((connection) => connection.nodes);
};
const findByUrl = async (context, asset_url) => {
// Verify that the asset_url is parsable.
@@ -111,7 +106,6 @@ module.exports = (context) => ({
findByUrl: (url) => findByUrl(context, url),
getByQuery: (query) => getAssetsByQuery(context, query),
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
getForMetrics: () => getAssetsForMetrics(context),
getAll: new util.SingletonResolver(() => AssetModel.find({}))
}
});
-2
View File
@@ -4,7 +4,6 @@ const debug = require('debug')('talk:graph:loaders');
const Actions = require('./actions');
const Assets = require('./assets');
const Comments = require('./comments');
const Metrics = require('./metrics');
const Settings = require('./settings');
const Tags = require('./tags');
const Users = require('./users');
@@ -17,7 +16,6 @@ let loaders = [
Actions,
Assets,
Comments,
Metrics,
Settings,
Tags,
Users,
-257
View File
@@ -1,257 +0,0 @@
const _ = require('lodash');
const DataLoader = require('dataloader');
const {objectCacheKeyFn} = require('./util');
const ActionModel = require('../../models/action');
const CommentModel = require('../../models/comment');
/**
* Returns the assets which have had comments made within the last time period.
*/
const getAssetActivityMetrics = ({loaders: {Assets}}, {from, to, limit}) => {
let assetMetrics = [];
return CommentModel.aggregate([
{$match: {
parent_id: null,
created_at: {
$gt: from,
$lt: to
}
}},
{$group: {
_id: '$asset_id',
commentCount: {
$sum: 1
}
}},
{$project: {
_id: false,
asset_id: '$_id',
commentCount: '$commentCount'
}},
{$sort: {
commentCount: -1
}},
{$limit: limit}
])
.then((results) => {
assetMetrics = results;
return Assets.getByID.loadMany(results.map((result) => result.asset_id));
})
.then((assets) => assets.map((asset, i) => {
// We're leveraging the fact that the comments returned by the aggregation
// query are in the request order that we just made, it's what the
// Assets.getByID loader does.
asset.commentCount = assetMetrics[i].commentCount;
return asset;
}));
};
/**
* Returns a list of assets with action metadata included on the models.
*/
const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sortBy, limit}) => {
// Get the recent actions.
let actionSummaries = await Metrics.getRecentActions.load({from, to});
let commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
if (action_type !== sortBy) {
return acc;
}
if (!(item_id in acc)) {
acc[item_id] = [];
}
acc[item_id].push({action_type, count});
return acc;
}, {});
// Collect just the comment id's.
let commentIDs = Object.keys(commentMetrics);
// Find those comments.
let comments = await Comments.get.loadMany(commentIDs);
let commentResults = _.groupBy(comments, 'asset_id');
let assetMetrics = Object.keys(commentResults)
.map((asset_id) => {
let ids = commentResults[asset_id].map((comment) => comment.id);
let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type');
let action_summaries = Object.keys(summaries).map((action_type) => ({
action_type,
actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0),
actionableItemCount: summaries[action_type].length
}));
return {action_summaries, id: asset_id};
})
.filter((asset) => {
let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sortBy));
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.
.sort((a, b) => {
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sortBy));
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sortBy));
// Both of them had an actionCount, hence we can determine that we could
// compare the actual values directly.
return bActionSummary.actionCount - aActionSummary.actionCount;
});
// Only keep the top `limit`.
assetMetrics = assetMetrics.slice(0, limit);
// Determine the assets that we need to return.
let assets = await Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id));
// Join up the assets that are returned by their id.
let groupedAssets = _.groupBy(assets, 'id');
// Return from the sorted asset metrics and return their assetes.
return assetMetrics.map(({id, action_summaries}) => {
if (id in groupedAssets) {
let asset = groupedAssets[id][0];
// Add the action summaries to the asset.
asset.action_summaries = action_summaries;
return asset;
}
return null;
}).filter((asset) => asset != null);
};
/**
* Returns a list of comments that are retrieved based on most activity within
* the indicated time range.
*/
const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sortBy, limit}) => {
let commentActionSummaries = {};
let actionSummaries = await Metrics.getRecentActions.load({from, to});
actionSummaries.sort((a, b) => {
let aActionSummary = a.action_type === sortBy ? a : null;
let bActionSummary = b.action_type === sortBy ? 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 === sortBy);
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.
let comments = await Comments.get.loadMany(commentIDs);
return 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([
// Find all actions that were created in the time range.
{$match: {
item_type: 'COMMENTS',
created_at: {
$gt: from,
$lt: to
}
}},
// Count all those items.
{$group: {
_id: {
item_id: '$item_id',
action_type: '$action_type'
},
count: {
$sum: 1
}
}},
// Project the count to a better field.
{$project: {
item_id: '$_id.item_id',
action_type: '$_id.action_type',
count: '$count'
}}
]);
};
module.exports = (context) => ({
Metrics: {
getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
batch: false,
cacheKeyFn: objectCacheKeyFn('from', 'to')
}),
Assets: {
get: ({from, to, sortBy, limit}) => getAssetMetrics(context, {from, to, sortBy, limit}),
getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}),
},
Comments: {
get: ({from, to, sortBy, limit}) => getCommentMetrics(context, {from, to, sortBy, limit}),
}
}
});
+56 -19
View File
@@ -1,6 +1,8 @@
const ActionsService = require('../../services/actions');
const errors = require('../../errors');
const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
const {
IGNORE_FLAGS_AGAINST_STAFF,
} = require('../../config');
/**
* getActionItem will return the item that is associated with the given action.
@@ -10,21 +12,21 @@ const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
* @param {Object} action the action being performed
* @return {Promise} resolves to the referenced item
*/
const getActionItem = async ({loaders: {Comments, Users}}, {item_id, item_type}) => {
if (item_type === 'COMMENTS') {
const comment = await Comments.get.load(item_id);
if (!comment) {
throw errors.ErrNotFound;
}
const getActionItem = async (ctx, {item_id, item_type}) => {
const {
loaders: {
Comments,
Users,
},
} = ctx;
return comment;
} else if (item_type === 'USERS') {
const user = await Users.getByID.load(item_id);
if (!user) {
throw errors.ErrNotFound;
}
return user;
switch (item_type) {
case 'COMMENTS':
return Comments.get.load(item_id);
case 'USERS':
return Users.getByID.load(item_id);
default:
return null;
}
};
@@ -37,10 +39,34 @@ const getActionItem = async ({loaders: {Comments, Users}}, {item_id, item_type})
* @return {Promise} resolves to the action created
*/
const createAction = async (ctx, {item_id, item_type, action_type, group_id, metadata = {}}) => {
const {user = {}, pubsub} = ctx;
const {
user = {},
pubsub,
connectors: {
services: {
Actions,
},
},
} = ctx;
// Gets the item referenced by the action.
const item = await getActionItem(ctx, {item_id, item_type});
if (!item || item === null) {
throw errors.ErrNotFound;
}
// If we are ignoring flags against staff, ensure that the target isn't a
// staff member.
if (IGNORE_FLAGS_AGAINST_STAFF) {
if (action_type === 'FLAG') {
// If the item is a user, and this is a flag. Check to see if they are
// staff, if they are, don't permit the flag.
if (item_type === 'USERS' && item.isStaff()) {
return null;
}
}
}
if (action_type === 'FLAG' && item_type === 'USERS') {
@@ -52,7 +78,7 @@ const createAction = async (ctx, {item_id, item_type, action_type, group_id, met
}
// Create the action itself.
let action = await ActionsService.create({
let action = await Actions.create({
item_id,
item_type,
user_id: user.id,
@@ -78,10 +104,21 @@ const createAction = async (ctx, {item_id, item_type, action_type, group_id, met
* @param {String} id the id of the action to delete
* @return {Promise} resolves to the deleted action, or null if not found.
*/
const deleteAction = ({user}, {id}) => ActionsService.delete({id, user_id: user.id});
const deleteAction = (ctx, {id}) => {
const {
user,
connectors: {
services: {
Actions,
},
},
} = ctx;
return Actions.delete({id, user_id: user.id});
};
module.exports = (ctx) => {
const mutators = {
let mutators = {
Action: {
create: () => Promise.reject(errors.ErrNotAuthorized),
delete: () => Promise.reject(errors.ErrNotAuthorized)
+13 -1
View File
@@ -15,7 +15,10 @@ const {
EDIT_COMMENT
} = require('../../perms/constants');
const debug = require('debug')('talk:graph:mutators:comment');
const {DISABLE_AUTOFLAG_SUSPECT_WORDS} = require('../../config');
const {
DISABLE_AUTOFLAG_SUSPECT_WORDS,
IGNORE_FLAGS_AGAINST_STAFF,
} = require('../../config');
const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => {
const item_type = 'COMMENTS';
@@ -295,6 +298,15 @@ const moderationPhases = [
}
},
// If a given user is a staff member, always approve their comment.
(context) => {
if (IGNORE_FLAGS_AGAINST_STAFF && context.user && context.user.isStaff()) {
return {
status: 'ACCEPTED',
};
}
},
// This phase checks the comment if it has any links in it if the check is
// enabled.
(context, comment, {assetSettings: {premodLinksEnable}}) => {
-22
View File
@@ -1,7 +1,6 @@
const {
SEARCH_ASSETS,
SEARCH_OTHERS_COMMENTS,
SEARCH_COMMENT_METRICS,
SEARCH_OTHER_USERS
} = require('../../perms/constants');
@@ -58,27 +57,6 @@ const RootQuery = {
return Users.getCountByQuery(query);
},
assetMetrics(_, query, {user, loaders: {Metrics: {Assets}}}) {
if (user == null || !user.can(SEARCH_ASSETS)) {
return null;
}
const {sortBy} = query;
if (sortBy === 'ACTIVITY') {
return Assets.getActivity(query);
}
return Assets.get(query);
},
commentMetrics(_, query, {user, loaders: {Metrics: {Comments}}}) {
if (user == null || !user.can(SEARCH_COMMENT_METRICS)) {
return null;
}
return Comments.get(query);
},
// This returns the current user, ensure that if we aren't logged in, we
// return null.
me(_, args, {user}) {
+2 -3
View File
@@ -5,9 +5,8 @@ const {
SEARCH_OTHER_USERS,
SEARCH_OTHERS_COMMENTS,
UPDATE_USER_ROLES,
SEARCH_COMMENT_METRICS,
LIST_OWN_TOKENS,
VIEW_USER_STATUS
VIEW_USER_STATUS,
} = require('../../perms/constants');
const User = {
@@ -79,7 +78,7 @@ const User = {
// Extract the reliability from the user metadata if they have permission.
reliable(user, _, {user: requestingUser}) {
if (requestingUser && requestingUser.can(SEARCH_COMMENT_METRICS)) {
if (requestingUser && requestingUser.can(SEARCH_ACTIONS)) {
return KarmaService.model(user);
}
},
-21
View File
@@ -923,19 +923,6 @@ enum SORT_COMMENTS_BY {
REPLIES
}
# Metrics for the assets.
enum ASSET_METRICS_SORT {
# Represents a FlagAction.
FLAG
# Represents a don't agree action.
DONTAGREE
# Represents activity.
ACTIVITY
}
type RootQuery {
# Site wide settings and defaults.
@@ -970,14 +957,6 @@ type RootQuery {
# a single User by id
user(id: ID!): User
# Asset metrics related to user actions are saturated into the assets
# returned. Parameters `from` and `to` are related to the action created_at field.
assetMetrics(from: Date!, to: Date!, sortBy: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
# Comment metrics related to user actions are saturated into the comments
# returned. Parameters `from` and `to` are related to the action created_at field.
commentMetrics(from: Date!, to: Date!, sortBy: ACTION_TYPE!, limit: Int = 10): [Comment!]
}
################################################################################