diff --git a/client/coral-admin/src/containers/Dashboard/ActivityWidget.js b/client/coral-admin/src/containers/Dashboard/ActivityWidget.js new file mode 100644 index 000000000..a6e293b82 --- /dev/null +++ b/client/coral-admin/src/containers/Dashboard/ActivityWidget.js @@ -0,0 +1,49 @@ +import React, {PropTypes} 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 ActivityWidget = ({assets}) => { + return ( +
+

Articles with the most conversations

+
+

{lang.t('streams.article')}

+

{lang.t('dashboard.comment_count')}

+
+
+ { + assets.length + ? assets.map(asset => { + return ( +
+ Moderate +

{asset.commentCount}

+ +

{asset.title}

+ +

{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

+
+ ); + }) + :
{lang.t('dashboard.no_activity')}
+ } +
+
+ ); +}; + +ActivityWidget.propTypes = { + assets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + url: PropTypes.string, + commentCount: PropTypes.number, + author: PropTypes.string, + created_at: PropTypes.string + })).isRequired +}; + +export default ActivityWidget; diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index 00af443d0..602a48d7f 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -4,7 +4,7 @@ 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 ActivityWidget from './ActivityWidget'; import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-admin/src/translations'; @@ -70,7 +70,7 @@ class Dashboard extends React.Component { return ; } - const {data: {assetsByLike, assetsByFlag}} = this.props; + const {data: {assetsByActivity, assetsByFlag}} = this.props; const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' || this.state.dashboardNote === 'hide'; // for Safari Incognito @@ -85,7 +85,7 @@ class Dashboard extends React.Component {

- +
); diff --git a/client/coral-admin/src/containers/Dashboard/FlagWidget.js b/client/coral-admin/src/containers/Dashboard/FlagWidget.js index 7d2ffe17a..468875d3d 100644 --- a/client/coral-admin/src/containers/Dashboard/FlagWidget.js +++ b/client/coral-admin/src/containers/Dashboard/FlagWidget.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import {Link} from 'react-router'; import styles from './Widget.css'; import I18n from 'coral-framework/modules/i18n/i18n'; @@ -6,8 +6,7 @@ import translations from 'coral-admin/src/translations'; const lang = new I18n(translations); -const FlagWidget = (props) => { - const {assets} = props; +const FlagWidget = ({assets}) => { return (
@@ -39,4 +38,14 @@ const FlagWidget = (props) => { ); }; +FlagWidget.propTypes = { + assets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + url: PropTypes.string, + action_summaries: PropTypes.array, + author: PropTypes.string, + created_at: PropTypes.string + })).isRequired +}; + export default FlagWidget; diff --git a/client/coral-admin/src/containers/Dashboard/LikeWidget.js b/client/coral-admin/src/containers/Dashboard/LikeWidget.js index 436fbcaf3..b878c281b 100644 --- a/client/coral-admin/src/containers/Dashboard/LikeWidget.js +++ b/client/coral-admin/src/containers/Dashboard/LikeWidget.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import {Link} from 'react-router'; import styles from './Widget.css'; import I18n from 'coral-framework/modules/i18n/i18n'; @@ -6,9 +6,7 @@ import translations from 'coral-admin/src/translations'; const lang = new I18n(translations); -const LikeWidget = (props) => { - - const {assets} = props; +const LikeWidget = ({assets}) => { return (
@@ -40,4 +38,14 @@ const LikeWidget = (props) => { ); }; +LikeWidget.propTypes = { + assets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + url: PropTypes.string, + action_summaries: PropTypes.array, + author: PropTypes.string, + created_at: PropTypes.string + })).isRequired +}; + export default LikeWidget; diff --git a/client/coral-admin/src/containers/Dashboard/Widget.css b/client/coral-admin/src/containers/Dashboard/Widget.css index 401cd1fe7..587c51ada 100644 --- a/client/coral-admin/src/containers/Dashboard/Widget.css +++ b/client/coral-admin/src/containers/Dashboard/Widget.css @@ -19,7 +19,6 @@ padding-left: 10px; font-size: 1.5rem; font-weight: bold; - background-color: #e0e0e0; } .widgetTable { diff --git a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql index 37335aeaa..c77fbc32b 100644 --- a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql +++ b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql @@ -4,6 +4,7 @@ fragment metrics on Asset { url author created_at + commentCount action_summaries { type: __typename actionCount diff --git a/client/coral-admin/src/graphql/queries/metricsQuery.graphql b/client/coral-admin/src/graphql/queries/metricsQuery.graphql index 42a9fb70e..f0dff8965 100644 --- a/client/coral-admin/src/graphql/queries/metricsQuery.graphql +++ b/client/coral-admin/src/graphql/queries/metricsQuery.graphql @@ -7,4 +7,7 @@ query Metrics ($from: Date!, $to: Date!) { assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) { ...metrics } + assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) { + ...metrics + } } diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 257148837..58788e546 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -122,6 +122,7 @@ "no_flags": "There have been no flags in the last 5 minutes! Hooray!", "no_likes": "There have been no likes in the last 5 minutes. All quiet.", "flags": "Flags", + "no_activity": "There haven't been any comments anywhere in the last five minutes.", "comment_count": "comments" }, "streams": { @@ -242,6 +243,7 @@ "no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!", "no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.", "flags": "Marcados", + "no_activity": "No hubo comentarios en los ultimos 5 minutos", "comment_count": "comentarios" }, "streams": { diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index a6fe5afe4..a0408ed4d 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -3,6 +3,53 @@ 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. @@ -208,10 +255,11 @@ module.exports = (context) => ({ cacheKeyFn: objectCacheKeyFn('from', 'to') }), Assets: { - get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit}) + get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit}), + getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}), }, Comments: { - get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit}) + get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit}), } } }); diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index bdcdcef78..e2836c51d 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -63,6 +63,10 @@ const RootQuery = { return null; } + if (sort === 'ACTIVITY') { + return Assets.getActivity({from, to, limit}); + } + return Assets.get({from, to, sort, limit}); }, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 3000ea091..e656d0183 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -479,6 +479,22 @@ enum USER_STATUS { APPROVED } +# Metrics for the assets. +enum ASSET_METRICS_SORT { + + # Represents a LikeAction. + LIKE + + # Represents a FlagAction. + FLAG + + # Represents a don't agree action. + DONTAGREE + + # Represents activity. + ACTIVITY +} + type RootQuery { # Site wide settings and defaults. @@ -506,7 +522,7 @@ type RootQuery { # Asset metrics related to user actions are saturated into the assets # returned. - assetMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset!] + assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!] # Comment metrics related to user actions are saturated into the comments # returned.