diff --git a/client/.eslintrc.json b/client/.eslintrc.json
index a935eaec6..1144f985d 100644
--- a/client/.eslintrc.json
+++ b/client/.eslintrc.json
@@ -18,6 +18,7 @@
],
"rules": {
"react/jsx-uses-react": "error",
- "react/jsx-uses-vars": "error"
+ "react/jsx-uses-vars": "error",
+ "no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js
index d96bacfe0..38d06dcb1 100644
--- a/client/coral-admin/src/AppRouter.js
+++ b/client/coral-admin/src/AppRouter.js
@@ -15,7 +15,7 @@ const routes = (
-
+
diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js
index c1d05d253..9fc308d2f 100644
--- a/client/coral-admin/src/actions/install.js
+++ b/client/coral-admin/src/actions/install.js
@@ -75,8 +75,7 @@ export const submitUser = () => (dispatch, getState) => {
dispatch(installRequest());
coralApi('/setup', {method: 'POST', body: data})
.then(result => {
- console.log(result);
- dispatch(installSuccess());
+ dispatch(installSuccess(result));
dispatch(nextStep());
})
.catch(error => {
diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js
deleted file mode 100644
index 187306c8c..000000000
--- a/client/coral-admin/src/components/FlagWidget.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, {PropTypes} from 'react';
-import {Link} from 'react-router';
-import styles from './FlagWidget.css';
-import I18n from 'coral-framework/modules/i18n/i18n';
-import translations from 'coral-admin/src/translations';
-
-const lang = new I18n(translations);
-
-const FlagWidget = ({assets}) => {
-
- return (
-
-
-
- | {/* empty on purpose */}
- {lang.t('streams.article')} |
- {lang.t('modqueue.flagged')} |
- {lang.t('modqueue.likes')} |
- {lang.t('dashboard.comment_count')} |
-
-
-
- {
- assets.length
- ? assets.map((asset, index) => {
- const flagSummary = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary');
- const likeSummary = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary');
- return (
-
- | {index + 1}. |
-
- {asset.title}
- {asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}
- |
- {flagSummary ? flagSummary.actionCount : 0} |
- {likeSummary ? likeSummary.actionCount : 0} |
- {asset.commentCount} |
-
- );
- })
- : | {lang.t('dashboard.no_flags')} |
- }
-
-
- );
-};
-
-FlagWidget.propTypes = {
- assets: PropTypes.arrayOf(
- PropTypes.shape({
- id: PropTypes.string,
- title: PropTypes.string,
- url: PropTypes.string,
- commentCount: PropTypes.number,
- action_summaries: PropTypes.arrayOf(
- PropTypes.shape({
- __typename: PropTypes.string.isRequired,
- actionCount: PropTypes.number.isRequired,
- actionableItemCount: PropTypes.number.isRequired
- })
- )
- })
- ).isRequired
-};
-
-export default FlagWidget;
diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.css b/client/coral-admin/src/containers/Dashboard/Dashboard.css
index be2cd9f7a..72b9bd90c 100644
--- a/client/coral-admin/src/containers/Dashboard/Dashboard.css
+++ b/client/coral-admin/src/containers/Dashboard/Dashboard.css
@@ -1,19 +1,5 @@
.Dashboard {
display: flex;
- padding: 5px;
-}
-
-.widget {
- margin-top: 10px;
- flex: 1;
- box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
- margin-right: 10px;
- padding: 15px;
-
-}
-
-.widget:last-child {
- margin-right: 0;
}
.heading {
diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js
index 6baa16af1..96e837a9e 100644
--- a/client/coral-admin/src/containers/Dashboard/Dashboard.js
+++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js
@@ -1,38 +1,46 @@
import React from 'react';
-import {compose} from 'react-apollo';
-import {mostFlags} from 'coral-admin/src/graphql/queries';
-import {Spinner} from 'coral-ui';
import styles from './Dashboard.css';
-import FlagWidget from '../../components/FlagWidget';
+import {compose} from 'react-apollo';
+import {connect} from 'react-redux';
+import {getMetrics} from 'coral-admin/src/graphql/queries';
+import FlagWidget from './FlagWidget';
+import LikeWidget from './LikeWidget';
+import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
+
+import {Spinner} from 'coral-ui';
class Dashboard extends React.Component {
+
render () {
- const {data} = this.props;
- const {metrics: assets} = data;
-
- if (data.loading) {
+ if (this.props.data && this.props.data.loading) {
return ;
}
- if (data.error) {
- return {data.error};
- }
+ const {data: {assetsByLike, assetsByFlag}} = this.props;
return (
-
-
Top Ten Articles with the most flagged comments
-
-
-
-
Top ten comments with the most likes
-
+
+
);
}
}
+const mapStateToProps = state => {
+ return {
+ settings: state.settings.toJS(),
+ moderation: state.moderation.toJS()
+ };
+};
+
+const mapDispatchToProps = dispatch => ({
+ showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
+ hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
+});
+
export default compose(
- mostFlags
+ connect(mapStateToProps, mapDispatchToProps),
+ getMetrics
)(Dashboard);
diff --git a/client/coral-admin/src/containers/Dashboard/FlagWidget.js b/client/coral-admin/src/containers/Dashboard/FlagWidget.js
new file mode 100644
index 000000000..537b89b8f
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/FlagWidget.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import {Link} from 'react-router';
+import styles from './Widget.css';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations';
+
+const lang = new I18n(translations);
+
+const FlagWidget = (props) => {
+ const {assets} = props;
+
+ return (
+
+
Articles with the most flags
+
+
+
+ | {/* empty on purpose */}
+ {lang.t('streams.article')} |
+ {lang.t('modqueue.flagged')} |
+
+
+
+ {
+ assets.length
+ ? assets.map((asset, index) => {
+ const flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
+ return (
+
+ | {index + 1}. |
+
+ {asset.title}
+ {asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}
+ |
+ {flagSummary ? flagSummary.actionCount : 0} |
+
+ );
+ })
+ : | {lang.t('dashboard.no_flags')} |
+ }
+
+
+
+ );
+};
+
+export default FlagWidget;
diff --git a/client/coral-admin/src/containers/Dashboard/LikeWidget.js b/client/coral-admin/src/containers/Dashboard/LikeWidget.js
new file mode 100644
index 000000000..ed6a0c0ae
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/LikeWidget.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import {Link} from 'react-router';
+import styles from './Widget.css';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations';
+
+const lang = new I18n(translations);
+
+const LikeWidget = (props) => {
+
+ const {assets} = props;
+
+ return (
+
+
Articles with the most likes
+
+
+
+ | {/* empty on purpose */}
+ {lang.t('streams.article')} |
+ {lang.t('modqueue.likes')} |
+
+
+
+ {
+ assets.length
+ ? assets.map((asset, index) => {
+ const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary');
+ return (
+
+ | {index + 1}. |
+
+ {asset.title}
+ {asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}
+ |
+ {likeSummary ? likeSummary.actionCount : 0} |
+
+ );
+ })
+ : | {lang.t('dashboard.no_likes')} |
+ }
+
+
+
+ );
+};
+
+export default LikeWidget;
diff --git a/client/coral-admin/src/containers/Dashboard/MostLikedCommentsWidget.js b/client/coral-admin/src/containers/Dashboard/MostLikedCommentsWidget.js
new file mode 100644
index 000000000..0c41ac7c6
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/MostLikedCommentsWidget.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations';
+import ModerationQueue from 'coral-admin/src/containers/ModerationQueue/ModerationQueue';
+import styles from './Widget.css';
+import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
+
+const lang = new I18n(translations);
+
+const MostLikedCommentsWidget = props => {
+ const {
+ comments,
+ moderation,
+ settings,
+ handleBanUser,
+ showBanUserDialog,
+ hideBanUserDialog,
+ acceptComment,
+ rejectComment
+ } = props;
+
+ return (
+
+
{lang.t('most_liked_comments')}
+
+
+
+ );
+};
+
+export default MostLikedCommentsWidget;
diff --git a/client/coral-admin/src/containers/Dashboard/Widget.css b/client/coral-admin/src/containers/Dashboard/Widget.css
new file mode 100644
index 000000000..624b72519
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/Widget.css
@@ -0,0 +1,42 @@
+.widget {
+ box-sizing: border-box;
+ margin: 10px 5px 5px 5px;
+ box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
+ padding: 15px;
+ flex: 1;
+}
+
+.heading {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: bold;
+}
+
+.widgetTable {
+ width: 100%;
+ border-collapse: collapse;
+ box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
+}
+
+.widgetTable thead th {
+ border-bottom: 1px solid #f47e6b;
+ padding: 10px;
+ text-align: left;
+}
+
+.widgetTable tbody tr {
+ border-bottom: 1px solid lightgrey;
+}
+
+.widgetTable tbody tr:last-child {
+ border-bottom: none;
+}
+
+.widgetTable tbody td {
+ padding: 10px;
+}
+
+.lede {
+ font-size: 0.9em;
+ color: grey;
+}
diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js
index d3e2f9194..19e6e7034 100644
--- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js
+++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js
@@ -65,6 +65,8 @@ class ModerationContainer extends Component {
}
}
+ const comments = data[activeTab];
+
return (
@@ -76,9 +78,8 @@ class ModerationContainer extends Component {
modQueueResort={modQueueResort}
/>
{
- const areComments = props.data[activeTab].length;
+const ModerationQueue = ({comments, ...props}) => {
return (
{
- areComments
- ? props.data[activeTab].map((comment, i) => {
+ comments.length
+ ? comments.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return {
};
ModerationQueue.propTypes = {
- data: PropTypes.object.isRequired,
- acceptComment: PropTypes.func.isRequired,
- rejectComment: PropTypes.func.isRequired,
- showBanUserDialog: PropTypes.func.isRequired,
+ suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
- suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
+ showBanUserDialog: PropTypes.func.isRequired,
+ rejectComment: PropTypes.func.isRequired,
+ acceptComment: PropTypes.func.isRequired,
+ comments: PropTypes.array.isRequired
};
export default ModerationQueue;
diff --git a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
new file mode 100644
index 000000000..37335aeaa
--- /dev/null
+++ b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
@@ -0,0 +1,12 @@
+fragment metrics on Asset {
+ id
+ title
+ url
+ author
+ created_at
+ action_summaries {
+ type: __typename
+ actionCount
+ actionableItemCount
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
index 809769a13..c4a30ad44 100644
--- a/client/coral-admin/src/graphql/queries/index.js
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -1,23 +1,7 @@
import {graphql} from 'react-apollo';
-import MOST_FLAGS from './mostFlags.graphql';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
-
-export const mostFlags = graphql(MOST_FLAGS, {
- options: () => {
-
- // currently hard-coded per Greg's advice
- const fiveMinutesAgo = new Date();
- fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 305);
- return {
- variables: {
- sort: 'FLAG',
- from: fiveMinutesAgo.toISOString(),
- to: new Date().toISOString()
- }
- };
- }
-});
+import METRICS from './metricsQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: ({params: {id = null}}) => {
@@ -34,6 +18,18 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
})
});
+export const getMetrics = graphql(METRICS, {
+ options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
+
+ return {
+ variables: {
+ from: dashboardWindowStart,
+ to: dashboardWindowEnd
+ }
+ };
+ }
+});
+
export const modQueueResort = (id, fetchMore) => (sort) => {
return fetchMore({
query: MOD_QUEUE_QUERY,
diff --git a/client/coral-admin/src/graphql/queries/metricsQuery.graphql b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
new file mode 100644
index 000000000..42a9fb70e
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/assetMetricsView.graphql"
+
+query Metrics ($from: Date!, $to: Date!) {
+ assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
+ ...metrics
+ }
+ assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
+ ...metrics
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql
deleted file mode 100644
index 182ea0c8b..000000000
--- a/client/coral-admin/src/graphql/queries/mostFlags.graphql
+++ /dev/null
@@ -1,14 +0,0 @@
-query Metrics ($from: Date!, $to: Date!, $sort: ACTION_TYPE!) {
- metrics(from: $from, to: $to, sort: $sort) {
- id
- title
- url
- commentCount
- author
- created_at
- action_summaries {
- actionCount
- actionableItemCount
- }
- }
-}
diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js
index a02cac198..f74eb659f 100644
--- a/client/coral-admin/src/reducers/settings.js
+++ b/client/coral-admin/src/reducers/settings.js
@@ -1,11 +1,22 @@
import {Map, List} from 'immutable';
import * as actions from '../actions/settings';
+// this is initialized here because
+// currently you have to reload the dashboard to get new stats
+// cleaner updates are planned in the future.
+// TODO: if there are more than two fields for the dashboard being created here,
+// please create a new reducer specifically for the Dashboard.
+const DASHBOARD_WINDOW_MINUTES = 5;
+let then = new Date();
+then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
+
const initialState = Map({
wordlist: Map({
banned: List(),
suspect: List()
}),
+ dashboardWindowStart: then.toISOString(),
+ dashboardWindowEnd: new Date().toISOString(),
domains: Map({
whitelist: List()
}),
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index 7219a22ca..92e5d098a 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -111,7 +111,8 @@
},
"dashboard": {
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
- "comment_count": "Comments"
+ "no_likes": "There have been no likes in the last 5 minutes. All quiet.",
+ "comment_count": "comments"
},
"streams": {
"empty_result": "No assets match this search. Maybe try widening your search?",
@@ -220,7 +221,8 @@
},
"dashbord": {
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
- "comment_count": "Comentarios"
+ "no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
+ "comment_count": "comentarios"
},
"streams": {
"empty_result": "No se encuentro articulo con esta busqueda. Tal vez extender la busqueda?",
diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js
index 6a781675b..216349412 100644
--- a/client/coral-sign-in/components/SignUpContent.js
+++ b/client/coral-sign-in/components/SignUpContent.js
@@ -138,12 +138,9 @@ class SignUpContent extends React.Component {
}
);
diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js
index b5f493831..6c9d4bcbc 100644
--- a/graph/loaders/comments.js
+++ b/graph/loaders/comments.js
@@ -39,7 +39,14 @@ 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 {Object}
+
+
+
+
+
+
+ graph context
* @param {Array} parent_ids the ids of parents for which there are
* comments that we want to get
*/
@@ -271,19 +278,33 @@ const genRecentComments = (_, ids) => {
};
/**
- * genComments returns the comments by the id's specified that are considered
- * public comments (i.e., accepted or not rejected).
+ * genComments returns the comments by the id's. Only admins can see non-public comments.
+ * @param {Object} context graph context
+ * @param {Array} ids the comment id's to fetch
+ * @return {Promise} resolves to the comments
*/
-const genComments = (context, ids) => {
- return CommentModel.find({
- id: {
- $in: ids
- },
- status: {
- $in: ['NONE', 'ACCEPTED']
- }
- })
- .then(util.singleJoinBy(ids, 'id'));
+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'));
+
+ */
+const genCommentsByID = (context, ids) => {
+
};
/**
diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js
index a842165c0..2627d82d9 100644
--- a/graph/loaders/metrics.js
+++ b/graph/loaders/metrics.js
@@ -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) {
+ 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})
+ }
}
});
diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js
index 43577dca0..2752ad0e3 100644
--- a/graph/resolvers/comment.js
+++ b/graph/resolvers/comment.js
@@ -32,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}}) {
diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js
index 6622fd17f..bdcdcef78 100644
--- a/graph/resolvers/root_query.js
+++ b/graph/resolvers/root_query.js
@@ -58,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
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index b9ab770ac..92f0d41e2 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -500,9 +500,13 @@ 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]
+ # 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!]
}
################################################################################
diff --git a/test/graph/loaders/metrics.js b/test/graph/loaders/metrics.js
new file mode 100644
index 000000000..29f2eeff1
--- /dev/null
+++ b/test/graph/loaders/metrics.js
@@ -0,0 +1,151 @@
+const {expect} = require('chai');
+const {graphql} = require('graphql');
+
+const schema = require('../../../graph/schema');
+const Context = require('../../../graph/context');
+const UserModel = require('../../../models/user');
+const AssetModel = require('../../../models/asset');
+const SettingsService = require('../../../services/settings');
+const ActionModel = require('../../../models/action');
+const CommentModel = require('../../../models/comment');
+
+describe('graph.loaders.Metrics', () => {
+ beforeEach(() => SettingsService.init());
+
+ describe('#Comments', () => {
+ const query = `
+ query CommentMetrics($from: Date!, $to: Date!) {
+ liked: commentMetrics(from: $from, to: $to, sort: LIKE) {
+ id
+ }
+ flagged: commentMetrics(from: $from, to: $to, sort: FLAG) {
+ id
+ }
+ }
+ `;
+
+ describe('different comment states', () => {
+
+ beforeEach(() => CommentModel.create([
+ {id: '1', body: 'a new comment!'},
+ {id: '2', body: 'a new comment!'},
+ {id: '3', body: 'a new comment!'}
+ ]));
+
+ [
+ {liked: 0, flagged: 0, actions: []},
+ {liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}]},
+ {liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]},
+ {liked: 1, flagged: 1, actions: [
+ {action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}
+ ]},
+ {liked: 3, flagged: 1, actions: [
+ {action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: '2', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: '3', item_type: 'COMMENTS'},
+ {action_type: 'FLAG', item_id: '3', item_type: 'COMMENTS'}
+ ]}
+ ].forEach(({liked, flagged, actions}) => {
+
+ describe(`with actions=${actions.length}`, () => {
+
+ beforeEach(() => ActionModel.create(actions));
+
+ it(`returns the correct amount of metrics liked=${liked} flagged=${flagged}`, () => {
+ const context = new Context({user: new UserModel({roles: ['ADMIN']})});
+
+ return graphql(schema, query, {}, context, {
+ from: (new Date()).setMinutes((new Date()).getMinutes() - 5),
+ to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
+ })
+ .then(({data, errors}) => {
+ expect(errors).to.be.undefined;
+ expect(data.liked).to.have.length(liked);
+ expect(data.flagged).to.have.length(flagged);
+ });
+ });
+
+ });
+
+ });
+
+ });
+ });
+
+ describe('#Assets', () => {
+ const query = `
+ fragment metrics on Asset {
+ id
+ action_summaries {
+ type: __typename
+ actionCount
+ actionableItemCount
+ }
+ }
+
+ query Metrics($from: Date!, $to: Date!) {
+ assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
+ ...metrics
+ }
+ assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
+ ...metrics
+ }
+ }
+ `;
+
+ describe('different comment states', () => {
+
+ beforeEach(() => Promise.all([
+ AssetModel.create([
+ {id: 'a1', url: 'http://localhost:3030/article/1'},
+ {id: 'a2', url: 'http://localhost:3030/article/2'}
+ ]),
+ CommentModel.create([
+ {id: 'c1', asset_id: 'a1', body: 'a new comment!'},
+ {id: 'c2', asset_id: 'a1', body: 'a new comment!'},
+ {id: 'c3', asset_id: 'a1', body: 'a new comment!'}
+ ])
+ ]));
+
+ [
+ {liked: 0, flagged: 0, actions: []},
+ {liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}]},
+ {liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]},
+ {liked: 1, flagged: 1, actions: [
+ {action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}
+ ]},
+ {liked: 1, flagged: 1, actions: [
+ {action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: 'c2', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: 'c3', item_type: 'COMMENTS'},
+ {action_type: 'FLAG', item_id: 'c3', item_type: 'COMMENTS'}
+ ]}
+ ].forEach(({liked, flagged, actions}) => {
+
+ describe(`with actions=${actions.length}`, () => {
+
+ beforeEach(() => ActionModel.create(actions));
+
+ it(`returns the correct amount of metrics liked=${liked} flagged=${flagged}`, () => {
+ const context = new Context({user: new UserModel({roles: ['ADMIN']})});
+
+ return graphql(schema, query, {}, context, {
+ from: (new Date()).setMinutes((new Date()).getMinutes() - 5),
+ to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
+ })
+ .then(({data, errors}) => {
+ expect(errors).to.be.undefined;
+ expect(data.assetsByLike).to.have.length(liked);
+ expect(data.assetsByFlag).to.have.length(flagged);
+ });
+ });
+
+ });
+
+ });
+
+ });
+ });
+});